Compare commits

...

52 Commits

Author SHA1 Message Date
Yu Sung f444f0bfb0 라이브
- 최대 스피커 수 방장 포함 6명으로 변경
2024-09-20 17:55:20 +09:00
Yu Sung ac304e7d17 크리에이터 채널 - 공유 버튼 변경
라이브 - 큰 음소거 이미지 변경
2024-09-20 17:46:29 +09:00
Yu Sung 3db59e6236 라이브 정보 수정
- 연령 제한 설정 추가
2024-09-11 23:26:17 +09:00
Yu Sung 8aa69f02fc 시리즈 콘텐츠 리스트
- 정렬(최신순, 등록순) 추가
2024-09-10 18:05:05 +09:00
Yu Sung 7c5b30335e 커뮤니티 댓글, 팬토크, 콘텐츠 댓글
- 프로필 이미지 터치시 차단, 신고가 가능한 유저 프로필 표시
2024-09-07 03:58:52 +09:00
Yu Sung 17ead38524 PG 결제 - 카카오페이 결제
- 구매자 정보 추가
2024-09-06 14:47:41 +09:00
Yu Sung bc8d3e6d70 라이브 방 - 유저 차단 팝업
- 리스너가 차단할 때 팝업 문구 수정
2024-09-05 20:02:50 +09:00
Yu Sung 66ce37defa 라이브 방
- 차단한 유저의 채팅이 보이지 않도록 수정
2024-09-05 18:12:10 +09:00
Yu Sung 7408288d85 차단 유저 리스트 페이지 추가 2024-09-04 18:00:11 +09:00
Yu Sung 2bea2365a0 마이 페이지
- 팔로잉 리스트 버튼 추가
2024-09-04 15:43:34 +09:00
Yu Sung f4d95e6755 댓글 입력창
- 우측에 불필요한 빈칸 제거
2024-08-30 18:00:46 +09:00
Yu Sung 30a480232b 비밀후원 체크박스 네모로 변경 2024-08-30 17:57:18 +09:00
Yu Sung 468a876e33 라이브 방
- 후원현황과 후원하기 버튼 순서 변경
2024-08-30 17:53:57 +09:00
Yu Sung 0f1de35e62 콘텐츠 상세 - 비밀댓글 등록
- 비밀댓글 체크박스 이미지 네모로 변경
2024-08-30 17:50:39 +09:00
Yu Sung 24f09d068d 콘텐츠 댓글 리스트
- 비밀댓글은 닉네임 옆에 '비밀댓글' 마크 추가
2024-08-30 17:47:44 +09:00
Yu Sung d1a90ad599 콘텐츠 댓글 리스트
- 유료 콘텐츠를 구매한 사람이 비밀댓글을 등록할 수 있는 기능 추가
2024-08-30 17:24:26 +09:00
Yu Sung fa849dd5b6 콘텐츠 상세
- 댓글이 없을 때 유료 콘텐츠를 구매한 사람이 비밀댓글을 등록할 수 있는 기능 추가
2024-08-30 17:15:13 +09:00
Yu Sung ca9dee5574 캔 충전 - 결제수단 카카오페이 추가 2024-08-30 17:00:05 +09:00
Yu Sung f5445a3c48 스플래시 2024/09 2024-08-27 00:06:10 +09:00
Yu Sung 534a6e737e 라이브 후원
- 비밀 후원 기능 추가
2024-08-26 20:07:37 +09:00
Yu Sung 81846f7f7b 콘텐츠 메인
- 우측 최상단에 콘텐츠 보관함
- 배너와 추천 시리즈 순서 변경
- 내 보관함 제거
2024-08-21 21:49:21 +09:00
Yu Sung 47cd685f80 라이브
- 후원 메시지, 룰렛 결과 모든 유저에게 보이도록 수정
2024-08-21 21:40:14 +09:00
Yu Sung 835ece8a6b 커뮤니티 오디오 녹음
- mode: videoRecording
- 재생 mode: moviePlayback
2024-08-08 03:10:07 +09:00
Yu Sung ef3494dcb1 커뮤니티 오디오 녹음
- 녹음시간이 3분이 되면 녹음 멈춤
2024-08-08 01:54:29 +09:00
Yu Sung 85a871693c 커뮤니티 오디오 녹음
- audioSession.setCategory mode videoRecording으로 수정
2024-08-08 01:34:14 +09:00
Yu Sung 0be8d0d98a 커뮤니티 게시물 등록
- 오디오 녹음 기능 추가
2024-08-07 23:49:52 +09:00
Yu Sung 22ab76d664 크리에이터 커뮤니티 게시글 전체리스트
- 오디오 재생기능 추가
2024-08-07 14:48:12 +09:00
Yu Sung a3beb9c9fe 온보딩 이미지 변경 2024-07-31 23:31:56 +09:00
Yu Sung e41549ea07 회사정보 고객센터 이용시간 추가 2024-07-31 18:31:33 +09:00
Yu Sung 89dc0b4540 앱 가이드 이미지 변경 2024-07-30 22:46:48 +09:00
Yu Sung b17a037b0a 앱 아이콘 변경 2024-07-30 22:17:14 +09:00
Yu Sung c1d63675db 스플래시 변경 2024-07-30 21:34:23 +09:00
Yu Sung 6d83869cae 휴대폰 결제도 헥토파이낸셜로 수정 2024-07-30 19:17:23 +09:00
Yu Sung 6884a060e4 휴대폰 결제가 아닌경우 user설정을 하도록 수정 2024-07-04 16:16:09 +09:00
Yu Sung 313af1949e 룰렛 합계 검증
- Float 값이라 정확히 100.0이 나오지 않으므로 totalPercentage가 99.9보다 크고 100.1보다 작으면 통과되도록 조건 수정
2024-07-02 22:12:05 +09:00
Yu Sung 9f402c8ec8 PG 수정
- 휴대폰 결제: 웰컴페이먼츠
- 나머지 : 헥토파이낸스(세틀뱅크)
2024-07-02 14:50:01 +09:00
Yu Sung 45b600ac41 통신판매업신고번호 추가 2024-06-29 18:35:21 +09:00
Yu Sung e1d42b5495 PG 수정
- 휴대폰 결제: 웰컴페이먼츠
- 나머지 : 헥토파이낸스(세틀뱅크)
2024-06-29 18:34:30 +09:00
Yu Sung 03d86ee2d5 라이브 공유하기 버튼 다시 추가 2024-06-28 13:30:55 +09:00
Yu Sung 9533e97f89 콘텐츠 등록
- 한정판 등록 기능 추가
2024-06-07 14:51:24 +09:00
Yu Sung 5fc0f1789a 콘텐츠 리스트
- 소장중 / 대여중 / Sold Out 표시
2024-06-04 19:32:26 +09:00
Yu Sung 9819f00d0d 크리에이터 커뮤니티 게시물
- 구매하지 않은 유료 게시물도 내용은 보이도록 수정
2024-05-30 13:25:47 +09:00
Yu Sung 27ee83f74b 스플래시 06월 2024-05-28 16:23:42 +09:00
Yu Sung 987c96bee8 커뮤니티 게시물
- 무료이거나 구매한 게시물은 이미지가 블러 처리 되지 않도록 수정
2024-05-27 22:52:35 +09:00
Yu Sung 76c20c2658 심사용 계정이 아닌 곳에서 가격 * 110으로 표시되는 버그 수정 2024-05-27 22:18:33 +09:00
Yu Sung db828cf44d 커뮤니티 게시글 구매 알림창
- 구매버튼 캔 이미지 제거
2024-05-25 00:12:59 +09:00
Yu Sung ee1664cf19 회사정보
- 대표 이메일 추가
2024-05-25 00:04:45 +09:00
Yu Sung 0a1b1865dd 콘텐츠 메인 탭
- 하단에 회사정보 추가
2024-05-24 23:38:04 +09:00
Yu Sung e7cbabb285 크리에이터 커뮤니티
- 텍스트뷰의 width를 screenSize().width - 42로 설정하여 아이템에 horizontal padding이 적용되지 않던 버그 수정
2024-05-24 22:28:35 +09:00
Yu Sung 3ae5ea776c 커뮤니티 유료 게시글 조회, 구매 기능 추가 2024-05-24 16:19:43 +09:00
Yu Sung 0a96509b35 커뮤니티 게시글 등록
- 유료 게시글 등록을 위해 가격 설정 추가
2024-05-24 15:11:40 +09:00
Yu Sung 2268b0c1bc 마이페이지 탭
- PG심사용 계정의 경우 캔 표시 영역 뷰 제거
2024-05-24 00:19:35 +09:00
123 changed files with 3194 additions and 622 deletions

View File

@ -6,7 +6,7 @@ target 'SodaLive' do
use_frameworks! use_frameworks!
# Pods for SodaLive # Pods for SodaLive
pod 'BootpayUI', '4.3.0' pod 'BootpayUI', '4.4.0'
pod 'ObjectBox' pod 'ObjectBox'
end end
@ -16,7 +16,7 @@ target 'SodaLive-dev' do
use_frameworks! use_frameworks!
# Pods for SodaLive-dev # Pods for SodaLive-dev
pod 'BootpayUI', '4.3.0' pod 'BootpayUI', '4.4.0'
pod 'ObjectBox' pod 'ObjectBox'
end end

View File

@ -1,27 +1,31 @@
PODS: PODS:
- Alamofire (5.7.1) - Alamofire (5.9.1)
- Bootpay (4.2.9): - Bootpay (4.4.4):
- CryptoSwift - CryptoSwift
- NVActivityIndicatorView
- ObjectMapper - ObjectMapper
- BootpayUI (4.3.0): - BootpayUI (4.4.0):
- Alamofire - Alamofire
- Bootpay (~> 4.2.8) - Bootpay (~> 4.4.0)
- CryptoSwift - CryptoSwift
- JGProgressHUD - JGProgressHUD
- ObjectMapper - ObjectMapper
- SCLAlertView - SCLAlertView
- SnapKit - SnapKit
- SwiftyJSON - SwiftyJSON
- CryptoSwift (1.7.1) - CryptoSwift (1.8.3)
- JGProgressHUD (2.2) - JGProgressHUD (2.2)
- NVActivityIndicatorView (5.2.0):
- NVActivityIndicatorView/Base (= 5.2.0)
- NVActivityIndicatorView/Base (5.2.0)
- ObjectBox (1.8.1) - ObjectBox (1.8.1)
- ObjectMapper (4.2.0) - ObjectMapper (4.4.2)
- SCLAlertView (0.8) - SCLAlertView (0.8)
- SnapKit (5.6.0) - SnapKit (5.7.1)
- SwiftyJSON (5.0.1) - SwiftyJSON (5.0.2)
DEPENDENCIES: DEPENDENCIES:
- BootpayUI (= 4.3.0) - BootpayUI (= 4.4.0)
- ObjectBox - ObjectBox
SPEC REPOS: SPEC REPOS:
@ -31,6 +35,7 @@ SPEC REPOS:
- BootpayUI - BootpayUI
- CryptoSwift - CryptoSwift
- JGProgressHUD - JGProgressHUD
- NVActivityIndicatorView
- ObjectBox - ObjectBox
- ObjectMapper - ObjectMapper
- SCLAlertView - SCLAlertView
@ -38,17 +43,18 @@ SPEC REPOS:
- SwiftyJSON - SwiftyJSON
SPEC CHECKSUMS: SPEC CHECKSUMS:
Alamofire: 0123a34370cb170936ae79a8df46cc62b2edeb88 Alamofire: f36a35757af4587d8e4f4bfa223ad10be2422b8c
Bootpay: d753088334a16ce99094142beb66a6610a15d84b Bootpay: ed9b04d0061931d4bb0c6a2e14dc44222168fde6
BootpayUI: 54dcbe59a23e0d91b07a8add8115e1a6deace0f0 BootpayUI: 58e4c9a23ffb65b8023ef9f3dcb1d70090599e69
CryptoSwift: d3d18dc357932f7e6d580689e065cf1f176007c1 CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483
JGProgressHUD: d83d7a981b85d11205e19ff8ad5bb9c40571c847 JGProgressHUD: d83d7a981b85d11205e19ff8ad5bb9c40571c847
NVActivityIndicatorView: fe52a6a68664c2df8991d7d9e3d86d8d19453c53
ObjectBox: a7900d5335218cd437cbc080b7ccc38a5211f7b4 ObjectBox: a7900d5335218cd437cbc080b7ccc38a5211f7b4
ObjectMapper: 1eb41f610210777375fa806bf161dc39fb832b81 ObjectMapper: e6e4d91ff7f2861df7aecc536c92d8363f4c9677
SCLAlertView: 6a77bb2edfc65e04dbe57725546cb4107a506b85 SCLAlertView: 6a77bb2edfc65e04dbe57725546cb4107a506b85
SnapKit: e01d52ebb8ddbc333eefe2132acf85c8227d9c25 SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
SwiftyJSON: 2f33a42c6fbc52764d96f13368585094bfd8aa5e SwiftyJSON: f5b1bf1cd8dd53cd25887ac0eabcfd92301c6a5a
PODFILE CHECKSUM: cdff30c96e85662f4de75ddd8d54358311c1e629 PODFILE CHECKSUM: 48980f586cd82e7704768a64fd3ca78a3c3cb0c6
COCOAPODS: 1.14.3 COCOAPODS: 1.15.2

View File

@ -1,7 +1,7 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "launcher_icon_1024px.png", "filename" : "launcher_1024x1024.png",
"idiom" : "universal", "idiom" : "universal",
"platform" : "ios", "platform" : "ios",
"size" : "1024x1024" "size" : "1024x1024"

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "btn_square_select_checked.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 B

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "btn_square_select_normal.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_content_keep.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_lock_bb.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "splash_title.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -4,11 +4,212 @@
<dict> <dict>
<key>FirebaseAppDelegateProxyEnabled</key> <key>FirebaseAppDelegateProxyEnabled</key>
<false/> <false/>
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-1299501215847962~8852459715</string>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
<true/> <true/>
</dict> </dict>
<key>SKAdNetworkItems</key>
<array>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>cstr6suwn9.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>4fzdc2evr5.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>4pfyvq9l8r.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>2fnua5tdw4.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>ydx93a7ass.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>5a6flpkh64.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>p78axxw29g.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>v72qych5uu.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>ludvb6z3bs.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>cp8zw746q7.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>3sh42y64q3.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>c6k4g5qg8m.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>s39g8k73mm.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>3qy4746246.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>f38h382jlk.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>hs6bdukanm.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>v4nxqhlyqp.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>wzmmz9fp6w.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>yclnxrl5pm.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>t38b2kh725.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>7ug5zh24hu.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>gta9lk7p23.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>vutu7akeur.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>y5ghdn5j9k.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>n6fk4nfna4.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>v9wttpbfk9.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>n38lu8286q.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>47vhws6wlr.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>kbd757ywx3.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>9t245vhmpl.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>eh6m2bh4zr.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>a2p9lx4jpn.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>22mmun2rn5.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>4468km3ulz.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>2u9pt9hc89.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>8s468mfl3y.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>klf5c3l5u5.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>ppxm28t8ap.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>ecpz2srf59.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>uw77j35x4d.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>pwa73g5rt2.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>mlmmfzh3r3.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>578prtvx9j.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>4dzt52r2t5.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>e5fvkxwrpn.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>8c4e2ghe7u.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>zq492l623r.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>3rd42ekr43.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>3qcr597p9d.skadnetwork</string>
</dict>
</array>
<key>UIAppFonts</key> <key>UIAppFonts</key>
<array> <array>
<string>gmarket_sans_bold.otf</string> <string>gmarket_sans_bold.otf</string>
@ -21,206 +222,5 @@
<string>fetch</string> <string>fetch</string>
<string>remote-notification</string> <string>remote-notification</string>
</array> </array>
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-1299501215847962~8852459715</string>
<key>SKAdNetworkItems</key>
<array>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>cstr6suwn9.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>4fzdc2evr5.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>4pfyvq9l8r.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>2fnua5tdw4.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>ydx93a7ass.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>5a6flpkh64.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>p78axxw29g.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>v72qych5uu.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>ludvb6z3bs.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>cp8zw746q7.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>3sh42y64q3.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>c6k4g5qg8m.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>s39g8k73mm.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>3qy4746246.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>f38h382jlk.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>hs6bdukanm.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>v4nxqhlyqp.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>wzmmz9fp6w.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>yclnxrl5pm.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>t38b2kh725.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>7ug5zh24hu.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>gta9lk7p23.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>vutu7akeur.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>y5ghdn5j9k.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>n6fk4nfna4.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>v9wttpbfk9.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>n38lu8286q.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>47vhws6wlr.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>kbd757ywx3.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>9t245vhmpl.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>eh6m2bh4zr.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>a2p9lx4jpn.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>22mmun2rn5.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>4468km3ulz.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>2u9pt9hc89.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>8s468mfl3y.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>klf5c3l5u5.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>ppxm28t8ap.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>ecpz2srf59.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>uw77j35x4d.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>pwa73g5rt2.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>mlmmfzh3r3.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>578prtvx9j.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>4dzt52r2t5.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>e5fvkxwrpn.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>8c4e2ghe7u.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>zq492l623r.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>3rd42ekr43.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>3qcr597p9d.skadnetwork</string>
</dict>
</array>
</dict> </dict>
</plist> </plist>

View File

@ -111,6 +111,23 @@ final class Agora {
rtmKit?.send(message, toPeer: peerId, completion: completion) rtmKit?.send(message, toPeer: peerId, completion: completion)
} }
func sendRawMessageToPeer(peerId: String, rawMessage: LiveRoomChatRawMessage, completion: AgoraRtmSendPeerMessageBlock? = nil, fail: (() -> Void)? = nil) {
let encoder = JSONEncoder()
let jsonMessageData = try? encoder.encode(rawMessage)
let option = AgoraRtmSendMessageOptions()
option.enableOfflineMessaging = false
option.enableHistoricalMessaging = false
if let jsonMessageData = jsonMessageData {
let message = AgoraRtmRawMessage(rawData: jsonMessageData, description: "")
rtmKit?.send(message, toPeer: peerId, sendMessageOptions: option, completion: completion)
} else {
if let fail = fail {
fail()
}
}
}
func mute(_ isMute: Bool) { func mute(_ isMute: Bool) {
rtcEngine?.muteLocalAudioStream(isMute) rtcEngine?.muteLocalAudioStream(isMute)
} }

View File

@ -129,4 +129,6 @@ enum AppStep {
case seriesContentAll(seriesId: Int, seriesTitle: String) case seriesContentAll(seriesId: Int, seriesTitle: String)
case tempCanPayment(orderType: OrderType, contentId: Int, title: String, can: Int) case tempCanPayment(orderType: OrderType, contentId: Int, title: String, can: Int)
case blockList
} }

View File

@ -57,6 +57,9 @@ struct SodaLiveApp: App {
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
ContentView() ContentView()
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in
CreatorCommunityMediaPlayerManager.shared.pauseContent()
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
UIApplication.shared.applicationIconBadgeNumber = 0 UIApplication.shared.applicationIconBadgeNumber = 0

View File

@ -91,7 +91,34 @@ struct ContentListItemView: View {
Spacer() Spacer()
if item.price > 0 { 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.isSoldOut {
Text("Sold Out")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color.grayd2)
.padding(.horizontal, 5.3)
.padding(.vertical, 2.7)
.overlay(
RoundedRectangle(cornerRadius: 2.6)
.stroke(Color.grayd2, lineWidth: 1)
)
.cornerRadius(2.6)
} else if item.price > 0 {
HStack(spacing: 8) { HStack(spacing: 8) {
Image("ic_can") Image("ic_can")
.resizable() .resizable()
@ -130,7 +157,10 @@ struct ContentListItemView_Previews: PreviewProvider {
commentCount: 0, commentCount: 0,
isPin: true, isPin: true,
isAdult: false, isAdult: false,
isScheduledToOpen: true isScheduledToOpen: true,
isRented: false,
isOwned: false,
isSoldOut: true
) )
) )
} }

View File

@ -87,7 +87,7 @@ extension ContentPlayManager {
do { do {
let audioSession = AVAudioSession.sharedInstance() let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .default) try audioSession.setCategory(.playback, mode: .moviePlayback)
try audioSession.setActive(true) try audioSession.setActive(true)
self.player = try AVAudioPlayer(data: audioData) self.player = try AVAudioPlayer(data: audioData)

View File

@ -26,8 +26,8 @@ final class ContentRepository {
return api.requestPublisher(.likeContent(request: PutAudioContentLikeRequest(contentId: audioContentId))) return api.requestPublisher(.likeContent(request: PutAudioContentLikeRequest(contentId: audioContentId)))
} }
func registerComment(audioContentId: Int, comment: String, parentId: Int? = nil) -> AnyPublisher<Response, MoyaError> { func registerComment(audioContentId: Int, comment: String, parentId: Int? = nil, isSecret: Bool = false) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.registerComment(request: RegisterAudioContentCommentRequest(comment: comment, contentId: audioContentId, parentId: parentId))) return api.requestPublisher(.registerComment(request: RegisterAudioContentCommentRequest(comment: comment, contentId: audioContentId, parentId: parentId, isSecret: isSecret)))
} }
func orderAudioContent(contentId: Int, orderType: OrderType) -> AnyPublisher<Response, MoyaError> { func orderAudioContent(contentId: Int, orderType: OrderType) -> AnyPublisher<Response, MoyaError> {

View File

@ -219,7 +219,7 @@ struct ContentCreateView: View {
VStack(spacing: 13.3) { VStack(spacing: 13.3) {
Text("소장 설정") Text("소장 설정")
.font(.custom(Font.bold.rawValue, size: 16.7)) .font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color.grayee)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 13.3) { HStack(spacing: 13.3) {
@ -241,7 +241,7 @@ struct ContentCreateView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
Text(viewModel.isOnlyRental ? "대여 가격" : "소장 가격") Text(viewModel.isOnlyRental ? "대여 가격" : "소장 가격")
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "d2d2d2")) .foregroundColor(Color.grayd2)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 0) { HStack(spacing: 0) {
@ -249,7 +249,7 @@ struct ContentCreateView: View {
.autocapitalization(.none) .autocapitalization(.none)
.disableAutocorrection(true) .disableAutocorrection(true)
.font(.custom(Font.bold.rawValue, size: 14.7)) .font(.custom(Font.bold.rawValue, size: 14.7))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color.grayee)
.cornerRadius(6.7) .cornerRadius(6.7)
.keyboardType(.numberPad) .keyboardType(.numberPad)
.padding(.trailing, 10) .padding(.trailing, 10)
@ -258,16 +258,16 @@ struct ContentCreateView: View {
Text("") Text("")
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color.gray77)
} }
.padding(.vertical, 17) .padding(.vertical, 17)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
.background(Color(hex: "222222")) .background(Color.gray22)
.cornerRadius(5.3) .cornerRadius(5.3)
.padding(.top, 5.3) .padding(.top, 5.3)
Rectangle() Rectangle()
.foregroundColor(Color(hex: "232323")) .foregroundColor(Color.gray23)
.frame(height: 1) .frame(height: 1)
.padding(.top, 11) .padding(.top, 11)
@ -289,6 +289,44 @@ struct ContentCreateView: View {
} }
.padding(.top, 26.7) .padding(.top, 26.7)
if viewModel.price > 0 && !viewModel.isOnlyRental {
VStack(spacing: 13.3) {
Text("한정판 설정")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color.grayee)
.frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 13.3) {
SelectButtonView(title: "무제한", isChecked: !viewModel.isLimited) {
if viewModel.isLimited {
viewModel.isLimited = false
}
}
SelectButtonView(title: "한정판", isChecked: viewModel.isLimited) {
if !viewModel.isLimited {
viewModel.isLimited = true
}
}
}
if viewModel.isLimited {
TextField("한정판 개수를 입력하세요", text: $viewModel.limitedString)
.autocapitalization(.none)
.disableAutocorrection(true)
.font(.custom(Font.bold.rawValue, size: 14.7))
.foregroundColor(Color.grayee)
.cornerRadius(6.7)
.keyboardType(.numberPad)
.padding(.vertical, 17)
.padding(.horizontal, 13.3)
.background(Color.gray22)
.cornerRadius(5.3)
}
}
.padding(.top, 26.7)
}
VStack(spacing: 13.3) { VStack(spacing: 13.3) {
Text("미리듣기") Text("미리듣기")
.font(.custom(Font.bold.rawValue, size: 16.7)) .font(.custom(Font.bold.rawValue, size: 16.7))

View File

@ -55,15 +55,48 @@ final class ContentCreateViewModel: ObservableObject {
didSet { didSet {
if isFree { if isFree {
priceString = "0" priceString = "0"
isLimited = false
isOnlyRental = false isOnlyRental = false
isGeneratePreview = true isGeneratePreview = true
} }
} }
} }
@Published var isOnlyRental = false @Published var isOnlyRental = false {
didSet {
if isOnlyRental {
isLimited = false
}
}
}
@Published var isGeneratePreview = true @Published var isGeneratePreview = true
@Published var isLimited = false {
didSet {
if !isLimited {
limitedString = ""
}
}
}
@Published var limited: Int? = nil {
didSet {
if let limited = limited, limited <= 0 {
limitedString = ""
}
}
}
@Published var limitedString: String = "" {
didSet {
if limitedString == "" {
limited = nil
} else {
limited = Int(limitedString)
}
}
}
@Published var previewStartTime: String = "" @Published var previewStartTime: String = ""
@Published var previewEndTime: String = "" @Published var previewEndTime: String = ""
@Published var releaseDateString: String = Date().convertDateFormat(dateFormat: "yyyy.MM.dd") @Published var releaseDateString: String = Date().convertDateFormat(dateFormat: "yyyy.MM.dd")
@ -92,6 +125,7 @@ final class ContentCreateViewModel: ObservableObject {
detail: detail, detail: detail,
tags: hashtags, tags: hashtags,
price: price, price: price,
limited: limited,
releaseDate: isActiveReservation ? "\(releaseDate.convertDateFormat(dateFormat: "yyyy-MM-dd")) \(releaseTime.convertDateFormat(dateFormat: "HH:mm"))" : nil, releaseDate: isActiveReservation ? "\(releaseDate.convertDateFormat(dateFormat: "yyyy-MM-dd")) \(releaseTime.convertDateFormat(dateFormat: "HH:mm"))" : nil,
timezone: TimeZone.current.identifier, timezone: TimeZone.current.identifier,
themeId: theme!.id, themeId: theme!.id,

View File

@ -12,6 +12,7 @@ struct CreateAudioContentRequest: Encodable {
let detail: String let detail: String
let tags: String let tags: String
let price: Int let price: Int
let limited: Int?
let releaseDate: String? let releaseDate: String?
let timezone: String let timezone: String
let themeId: Int let themeId: Int

View File

@ -17,6 +17,7 @@ struct AudioContentCommentItemView: View {
let modifyComment: (Int, String) -> Void let modifyComment: (Int, String) -> Void
let onClickDelete: (Int) -> Void let onClickDelete: (Int) -> Void
let onClickProfile: (Int) -> Void
@State var isShowPopupMenu: Bool = false @State var isShowPopupMenu: Bool = false
@State var isModeModify: Bool = false @State var isModeModify: Bool = false
@ -30,15 +31,32 @@ struct AudioContentCommentItemView: View {
.resizable() .resizable()
.frame(width: 40, height: 40) .frame(width: 40, height: 40)
.clipShape(Circle()) .clipShape(Circle())
.onTapGesture {
if UserDefaults.int(forKey: .userId) != commentItem.writerId {
onClickProfile(commentItem.writerId)
}
}
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
Text(commentItem.nickname) HStack(spacing: 6.7) {
.font(.custom(Font.medium.rawValue, size: 12)) Text(commentItem.nickname)
.foregroundColor(Color.gray90) .font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color.gray90)
if commentItem.isSecret {
Text("비밀댓글")
.font(.custom(Font.medium.rawValue, size: 11))
.foregroundColor(Color.grayee)
.padding(.horizontal, 4)
.padding(.vertical, 2)
.background(Color.button.opacity(0.2))
.cornerRadius(3.3)
}
}
Text(commentItem.date) Text(commentItem.date)
.font(.custom(Font.medium.rawValue, size: 10.3)) .font(.custom(Font.medium.rawValue, size: 10.3))
.foregroundColor(Color(hex: "525252")) .foregroundColor(Color.gray52)
.padding(.top, 4) .padding(.top, 4)
} }
@ -83,8 +101,8 @@ struct AudioContentCommentItemView: View {
.autocapitalization(.none) .autocapitalization(.none)
.disableAutocorrection(true) .disableAutocorrection(true)
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color.grayee)
.accentColor(Color(hex: "3bb9f1")) .accentColor(Color.button)
.keyboardType(.default) .keyboardType(.default)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
@ -102,7 +120,7 @@ struct AudioContentCommentItemView: View {
isModeModify = false isModeModify = false
} }
} }
.background(Color(hex: "232323")) .background(Color.gray23)
.cornerRadius(10) .cornerRadius(10)
.overlay( .overlay(
RoundedRectangle(cornerRadius: 10) RoundedRectangle(cornerRadius: 10)
@ -139,7 +157,7 @@ struct AudioContentCommentItemView: View {
.padding(.leading, 46.7) .padding(.leading, 46.7)
Rectangle() Rectangle()
.foregroundColor(Color(hex: "595959")) .foregroundColor(Color.gray59)
.frame(height: 0.5) .frame(height: 0.5)
.padding(.top, 16.7) .padding(.top, 16.7)
} }
@ -149,7 +167,7 @@ struct AudioContentCommentItemView: View {
if commentItem.writerId == UserDefaults.int(forKey: .userId) { if commentItem.writerId == UserDefaults.int(forKey: .userId) {
Text("수정") Text("수정")
.font(.custom(Font.medium.rawValue, size: 14)) .font(.custom(Font.medium.rawValue, size: 14))
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color.gray77)
.onTapGesture { .onTapGesture {
isModeModify = true isModeModify = true
isShowPopupMenu = false isShowPopupMenu = false
@ -161,7 +179,7 @@ struct AudioContentCommentItemView: View {
{ {
Text("삭제") Text("삭제")
.font(.custom(Font.medium.rawValue, size: 14)) .font(.custom(Font.medium.rawValue, size: 14))
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color.gray77)
.onTapGesture { .onTapGesture {
onClickDelete(commentItem.id) onClickDelete(commentItem.id)
isShowPopupMenu = false isShowPopupMenu = false
@ -169,7 +187,7 @@ struct AudioContentCommentItemView: View {
} }
} }
.padding(10) .padding(10)
.background(Color(hex: "222222")) .background(Color.gray22)
} }
} }
.onAppear { comment = commentItem.comment } .onAppear { comment = commentItem.comment }

View File

@ -14,12 +14,16 @@ struct AudioContentCommentListView: View {
let creatorId: Int let creatorId: Int
let audioContentId: Int let audioContentId: Int
let isShowSecret: Bool
@StateObject var viewModel = AudioContentCommentListViewModel() @StateObject var viewModel = AudioContentCommentListViewModel()
@State private var commentId: Int = 0 @State private var commentId: Int = 0
@State private var isShowDeletePopup: Bool = false @State private var isShowDeletePopup: Bool = false
@State private var memberId: Int = 0
@State private var isShowMemberProfilePopup: Bool = false
var body: some View { var body: some View {
NavigationView { NavigationView {
ZStack { ZStack {
@ -50,6 +54,28 @@ struct AudioContentCommentListView: View {
.padding(.bottom, 13.3) .padding(.bottom, 13.3)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
if isShowSecret {
HStack(spacing: 8) {
Spacer()
Image(viewModel.isSecret ? "btn_square_select_checked" : "btn_square_select_normal")
.resizable()
.frame(width: 20, height: 20)
.onTapGesture {
viewModel.isSecret.toggle()
}
Text("비밀댓글")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(viewModel.isSecret ? Color.button : Color.grayee)
.onTapGesture {
viewModel.isSecret.toggle()
}
}
.padding(.bottom, 13.3)
.padding(.horizontal, 13.3)
}
HStack(spacing: 8) { HStack(spacing: 8) {
KFImage(URL(string: UserDefaults.string(forKey: .profileImage))) KFImage(URL(string: UserDefaults.string(forKey: .profileImage)))
.cancelOnDisappear(true) .cancelOnDisappear(true)
@ -63,8 +89,8 @@ struct AudioContentCommentListView: View {
.autocapitalization(.none) .autocapitalization(.none)
.disableAutocorrection(true) .disableAutocorrection(true)
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color.grayee)
.accentColor(Color(hex: "3bb9f1")) .accentColor(Color.button)
.keyboardType(.default) .keyboardType(.default)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
@ -79,20 +105,18 @@ struct AudioContentCommentListView: View {
viewModel.registerComment() viewModel.registerComment()
} }
} }
.background(Color(hex: "232323")) .background(Color.gray23)
.cornerRadius(10) .cornerRadius(10)
.overlay( .overlay(
RoundedRectangle(cornerRadius: 10) RoundedRectangle(cornerRadius: 10)
.strokeBorder(lineWidth: 1) .strokeBorder(lineWidth: 1)
.foregroundColor(Color(hex: "3bb9f1")) .foregroundColor(Color.button)
) )
Spacer()
} }
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
Rectangle() Rectangle()
.foregroundColor(Color(hex: "595959")) .foregroundColor(Color.gray59)
.frame(height: 0.5) .frame(height: 0.5)
.padding(.top, 12) .padding(.top, 12)
.padding(.bottom, 13.3) .padding(.bottom, 13.3)
@ -116,6 +140,10 @@ struct AudioContentCommentListView: View {
onClickDelete: { onClickDelete: {
commentId = $0 commentId = $0
isShowDeletePopup = true isShowDeletePopup = true
},
onClickProfile: {
memberId = $0
isShowMemberProfilePopup = true
} }
) )
.padding(.horizontal, 26.7) .padding(.horizontal, 26.7)
@ -148,6 +176,10 @@ struct AudioContentCommentListView: View {
) )
} }
if isShowMemberProfilePopup {
MemberProfileDialog(isShowing: $isShowMemberProfilePopup, memberId: memberId)
}
if viewModel.isLoading { if viewModel.isLoading {
LoadingView() LoadingView()
} }

View File

@ -22,6 +22,8 @@ class AudioContentCommentListViewModel: ObservableObject {
@Published var totalCommentCount = 0 @Published var totalCommentCount = 0
@Published var commentList = [GetAudioContentCommentListItem]() @Published var commentList = [GetAudioContentCommentListItem]()
@Published var isSecret = false
var audioContentId = 0 var audioContentId = 0
var page = 1 var page = 1
var isLast = false var isLast = false
@ -84,7 +86,7 @@ class AudioContentCommentListViewModel: ObservableObject {
isLoading = true isLoading = true
repository.registerComment(audioContentId: audioContentId, comment: comment) repository.registerComment(audioContentId: audioContentId, comment: comment, isSecret: isSecret)
.sink { result in .sink { result in
switch result { switch result {
case .finished: case .finished:

View File

@ -20,6 +20,9 @@ struct AudioContentListReplyView: View {
@State private var commentId: Int = 0 @State private var commentId: Int = 0
@State private var isShowDeletePopup: Bool = false @State private var isShowDeletePopup: Bool = false
@State private var memberId: Int = 0
@State private var isShowMemberProfilePopup: Bool = false
var body: some View { var body: some View {
ZStack { ZStack {
VStack(spacing: 0) { VStack(spacing: 0) {
@ -98,7 +101,11 @@ struct AudioContentListReplyView: View {
isReplyComment: true, isReplyComment: true,
isShowPopupMenuButton: false, isShowPopupMenuButton: false,
modifyComment: { _, _ in }, modifyComment: { _, _ in },
onClickDelete: { _ in } onClickDelete: { _ in },
onClickProfile: {
memberId = $0
isShowMemberProfilePopup = true
}
) )
.padding(.horizontal, 26.7) .padding(.horizontal, 26.7)
.padding(.bottom, 13.3) .padding(.bottom, 13.3)
@ -120,6 +127,10 @@ struct AudioContentListReplyView: View {
onClickDelete: { onClickDelete: {
commentId = $0 commentId = $0
isShowDeletePopup = true isShowDeletePopup = true
},
onClickProfile: {
memberId = $0
isShowMemberProfilePopup = true
} }
) )
.padding(.horizontal, 40) .padding(.horizontal, 40)
@ -153,6 +164,10 @@ struct AudioContentListReplyView: View {
) )
} }
if isShowMemberProfilePopup {
MemberProfileDialog(isShowing: $isShowMemberProfilePopup, memberId: memberId)
}
if viewModel.isLoading { if viewModel.isLoading {
LoadingView() LoadingView()
} }

View File

@ -12,10 +12,12 @@ struct ContentDetailCommentView: View {
let commentCount: Int let commentCount: Int
let commentList: [GetAudioContentCommentListItem] let commentList: [GetAudioContentCommentListItem]
let isShowSecret: Bool
let registerComment: (String) -> Void let registerComment: (String, Bool) -> Void
@State private var comment = "" @State private var comment = ""
@State private var isSecret = false
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 10.3) { VStack(alignment: .leading, spacing: 10.3) {
@ -26,9 +28,24 @@ struct ContentDetailCommentView: View {
Text("\(commentCount)") Text("\(commentCount)")
.font(.custom(Font.medium.rawValue, size: 12)) .font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "909090")) .foregroundColor(Color.gray90)
Spacer() Spacer()
if isShowSecret && commentCount <= 0 {
HStack(spacing: 8) {
Image(isSecret ? "btn_square_select_checked" : "btn_square_select_normal")
.resizable()
.frame(width: 20, height: 20)
Text("비밀댓글")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(isSecret ? Color.button : Color.grayee)
}
.onTapGesture {
isSecret.toggle()
}
}
} }
HStack(spacing: 8) { HStack(spacing: 8) {
@ -48,7 +65,7 @@ struct ContentDetailCommentView: View {
if commentCount > 0 { if commentCount > 0 {
Text(commentList[0].comment) Text(commentList[0].comment)
.font(.custom(Font.medium.rawValue, size: 12)) .font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "bbbbbb")) .foregroundColor(Color.graybb)
.lineLimit(1) .lineLimit(1)
.lineSpacing(8) .lineSpacing(8)
.padding(.leading, 3) .padding(.leading, 3)
@ -58,8 +75,8 @@ struct ContentDetailCommentView: View {
.autocapitalization(.none) .autocapitalization(.none)
.disableAutocorrection(true) .disableAutocorrection(true)
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color.grayee)
.accentColor(Color(hex: "3bb9f1")) .accentColor(Color.button)
.keyboardType(.default) .keyboardType(.default)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
@ -71,15 +88,15 @@ struct ContentDetailCommentView: View {
.padding(6.7) .padding(6.7)
.onTapGesture { .onTapGesture {
hideKeyboard() hideKeyboard()
registerComment(comment) registerComment(comment, isSecret)
} }
} }
.background(Color(hex: "232323")) .background(Color.gray23)
.cornerRadius(10) .cornerRadius(10)
.overlay( .overlay(
RoundedRectangle(cornerRadius: 10) RoundedRectangle(cornerRadius: 10)
.strokeBorder(lineWidth: 1) .strokeBorder(lineWidth: 1)
.foregroundColor(Color(hex: "3bb9f1")) .foregroundColor(Color.button)
) )
} }

View File

@ -18,6 +18,7 @@ struct GetAudioContentCommentListItem: Decodable {
let nickname: String let nickname: String
let profileUrl: String let profileUrl: String
let comment: String let comment: String
let isSecret: Bool
let donationCan: Int let donationCan: Int
let date: String let date: String
let replyCount: Int let replyCount: Int

View File

@ -11,4 +11,5 @@ struct RegisterAudioContentCommentRequest: Encodable {
let comment: String let comment: String
let contentId: Int let contentId: Int
let parentId: Int? let parentId: Int?
let isSecret: Bool
} }

View File

@ -136,8 +136,9 @@ struct ContentDetailView: View {
ContentDetailCommentView( ContentDetailCommentView(
commentCount: audioContent.commentCount, commentCount: audioContent.commentCount,
commentList: audioContent.commentList, commentList: audioContent.commentList,
registerComment: { comment in isShowSecret: audioContent.existOrdered,
self.viewModel.registerComment(comment: comment) registerComment: { comment, isSecret in
self.viewModel.registerComment(comment: comment, isSecret: isSecret)
} }
) )
.padding(10.3) .padding(10.3)
@ -233,7 +234,7 @@ struct ContentDetailView: View {
orderType: orderType, orderType: orderType,
contentId: audioContent.contentId, contentId: audioContent.contentId,
title: audioContent.title, title: audioContent.title,
can: orderType == .RENTAL ? Int(ceil(Double(audioContent.price) * 0.6)) : audioContent.price can: !audioContent.isOnlyRental && orderType == .RENTAL ? Int(ceil(Double(audioContent.price) * 0.6)) : audioContent.price
) )
) )
} else { } else {
@ -323,7 +324,7 @@ struct ContentDetailView: View {
} }
if viewModel.isShowDonationPopup { if viewModel.isShowDonationPopup {
LiveRoomDonationDialogView(isShowing: $viewModel.isShowDonationPopup, isAudioContentDonation: true) { can, comment in LiveRoomDonationDialogView(isShowing: $viewModel.isShowDonationPopup, isAudioContentDonation: true) { can, comment, _ in
viewModel.donation(can: can, comment: comment) viewModel.donation(can: can, comment: comment)
} }
} }
@ -360,7 +361,8 @@ struct ContentDetailView: View {
AudioContentCommentListView( AudioContentCommentListView(
isPresented: $isShowCommentListView, isPresented: $isShowCommentListView,
creatorId: viewModel.audioContent!.creator.creatorId, creatorId: viewModel.audioContent!.creator.creatorId,
audioContentId: viewModel.audioContent!.contentId audioContentId: viewModel.audioContent!.contentId,
isShowSecret: viewModel.audioContent!.existOrdered
) )
} }
) )

View File

@ -239,14 +239,14 @@ final class ContentDetailViewModel: ObservableObject {
} }
} }
func registerComment(comment: String) { func registerComment(comment: String, isSecret: Bool) {
if comment.trimmingCharacters(in: .whitespaces).isEmpty { if comment.trimmingCharacters(in: .whitespaces).isEmpty {
return return
} }
isLoading = true isLoading = true
repository.registerComment(audioContentId: contentId, comment: comment) repository.registerComment(audioContentId: contentId, comment: comment, isSecret: isSecret)
.sink { result in .sink { result in
switch result { switch result {
case .finished: case .finished:

View File

@ -46,9 +46,15 @@ struct ContentOrderDialogView: View {
.frame(width: 16.7, height: 16.7) .frame(width: 16.7, height: 16.7)
} }
Text(isOnlyRental ? "\(price * 110)" : "\(Int(ceil(Double(price) * 0.6)) * 110)") if UserDefaults.int(forKey: .userId) == 17958 {
.font(.custom(Font.bold.rawValue, size: 13.3)) Text(isOnlyRental ? "\(price * 110)" : "\(Int(ceil(Double(price) * 0.6)) * 110)")
.foregroundColor(Color.grayee) .font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color.grayee)
} else {
Text(isOnlyRental ? "\(price)" : "\(Int(ceil(Double(price) * 0.6)))")
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color.grayee)
}
if UserDefaults.int(forKey: .userId) == 17958 { if UserDefaults.int(forKey: .userId) == 17958 {
Text("") Text("")
@ -87,9 +93,15 @@ struct ContentOrderDialogView: View {
.frame(width: 16.7, height: 16.7) .frame(width: 16.7, height: 16.7)
} }
Text("\(price * 110)") if UserDefaults.int(forKey: .userId) == 17958 {
.font(.custom(Font.bold.rawValue, size: 13.3)) Text("\(price * 110)")
.foregroundColor(Color.grayee) .font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color.grayee)
} else {
Text("\(price)")
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color.grayee)
}
if UserDefaults.int(forKey: .userId) == 17958 { if UserDefaults.int(forKey: .userId) == 17958 {
Text("") Text("")

View File

@ -18,10 +18,11 @@ struct LiveRoomDonationDialogView: View {
@State private var donationMessage = "" @State private var donationMessage = ""
@State private var isShowErrorPopup = false @State private var isShowErrorPopup = false
@State private var errorMessage = "" @State private var errorMessage = ""
@State private var isSecret = false
@Binding var isShowing: Bool @Binding var isShowing: Bool
let isAudioContentDonation: Bool let isAudioContentDonation: Bool
let onClickDonation: (Int, String) -> Void let onClickDonation: (Int, String, Bool) -> Void
@StateObject var keyboardHandler = KeyboardHandler() @StateObject var keyboardHandler = KeyboardHandler()
@ -82,6 +83,27 @@ struct LiveRoomDonationDialogView: View {
.foregroundColor(Color.gray90) .foregroundColor(Color.gray90)
.padding(.top, 16) .padding(.top, 16)
if !isAudioContentDonation {
HStack(spacing: 0) {
Spacer()
HStack(spacing: 8) {
Image(isSecret ? "btn_square_select_checked" : "btn_square_select_normal")
.resizable()
.frame(width: 20, height: 20)
Text("비밀후원")
.font(.custom(Font.medium.rawValue, size: 14.7))
.foregroundColor(isSecret ? Color.button : Color.grayee)
}
.onTapGesture {
isSecret.toggle()
}
}
.padding(.horizontal, 20)
.padding(.top, 16)
}
TextField("몇 캔을 후원할까요?", text: $donationCan) TextField("몇 캔을 후원할까요?", text: $donationCan)
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
@ -221,7 +243,7 @@ struct LiveRoomDonationDialogView: View {
.onTapGesture { .onTapGesture {
if !donationCan.trimmingCharacters(in: .whitespaces).isEmpty, if !donationCan.trimmingCharacters(in: .whitespaces).isEmpty,
let can = Int(donationCan) { let can = Int(donationCan) {
onClickDonation(can, donationMessage) onClickDonation(can, donationMessage, isSecret)
isShowing = false isShowing = false
} else { } else {
errorMessage = "1캔 이상 후원하실 수 있습니다." errorMessage = "1캔 이상 후원하실 수 있습니다."

View File

@ -17,19 +17,28 @@ struct ContentMainView: View {
ScrollView(.vertical, showsIndicators: false) { ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
Text("콘텐츠 마켓") HStack(spacing: 0) {
.font(.custom(Font.bold.rawValue, size: 21.3)) Text("콘텐츠 마켓")
.foregroundColor(Color(hex: "3bb9f1")) .font(.custom(Font.bold.rawValue, size: 21.3))
.padding(.bottom, 26.7) .foregroundColor(Color.button)
.padding(.horizontal, 13.3)
Spacer()
Image("ic_content_keep")
.onTapGesture {
AppState.shared.setAppStep(step: .orderListAll)
}
}
.padding(.bottom, 26.7)
.padding(.horizontal, 13.3)
if !viewModel.isLoading { if !viewModel.isLoading {
ContentMainRecommendSeriesView()
ContentMainBannerView() ContentMainBannerView()
.padding(.bottom, 26.7) .padding(.bottom, 26.7)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
ContentMainRecommendSeriesView()
HStack(spacing: 8) { HStack(spacing: 8) {
ZStack { ZStack {
Image("img_bg_short_play") Image("img_bg_short_play")
@ -78,9 +87,6 @@ struct ContentMainView: View {
.padding(.bottom, 40) .padding(.bottom, 40)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
ContentMainMyStashView()
.padding(.horizontal, 13.3)
ContentMainNewContentView() ContentMainNewContentView()
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
@ -92,6 +98,26 @@ struct ContentMainView: View {
ContentMainCurationView() ContentMainCurationView()
.padding(.top, 40) .padding(.top, 40)
.padding(.bottom, 20) .padding(.bottom, 20)
Text("""
- :
- :
- : 335 10, 5 563A호
- : 870-81-03220
- : 2024-B-1012
- : 02.2055.1477 ( 10:00~19:00)
- : sodalive.official@gmail.com
""")
.font(.custom(Font.medium.rawValue, size: 11))
.foregroundColor(Color.gray77)
.padding(.top, 13.3)
.padding(.horizontal, 13.3)
} }
} }
.padding(.vertical, 13.3) .padding(.vertical, 13.3)

View File

@ -20,6 +20,37 @@ struct SeriesContentAllView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
DetailNavigationBar(title: "\(seriesTitle) - 전체회차 듣기") DetailNavigationBar(title: "\(seriesTitle) - 전체회차 듣기")
HStack(spacing: 13.3) {
Spacer()
Text("최신순")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(
Color.graye2
.opacity(viewModel.sortType == .NEWEST ? 1 : 0.5)
)
.onTapGesture {
if viewModel.sortType != .NEWEST {
viewModel.sortType = .NEWEST
}
}
Text("등록순")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(
Color.graye2
.opacity(viewModel.sortType == .OLDEST ? 1 : 0.5)
)
.onTapGesture {
if viewModel.sortType != .OLDEST {
viewModel.sortType = .OLDEST
}
}
}
.padding(.vertical, 13.3)
.padding(.horizontal, 20)
.background(Color.gray16)
ScrollView(.vertical, showsIndicators: false) { ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 12) { VStack(spacing: 12) {
ForEach(0..<viewModel.seriesContentList.count, id: \.self) { index in ForEach(0..<viewModel.seriesContentList.count, id: \.self) { index in

View File

@ -19,14 +19,24 @@ final class SeriesContentAllViewModel: ObservableObject {
@Published var isShowPopup = false @Published var isShowPopup = false
@Published var seriesContentList = [GetSeriesContentListItem]() @Published var seriesContentList = [GetSeriesContentListItem]()
@Published var sortType: SeriesListAllViewModel.SeriesSortType = .NEWEST {
didSet {
page = 1
isLast = false
getSeriesContentList()
}
}
var page = 1 var page = 1
var isLast = false var isLast = false
private let pageSize = 10 private let pageSize = 10
func getSeriesContentList() { func getSeriesContentList() {
if !isLoading && !isLast { if !isLoading && !isLast {
isLoading = true
repository repository
.getSeriesContentList(seriesId: seriesId, page: page, size: pageSize) .getSeriesContentList(seriesId: seriesId, page: page, size: pageSize, sortType: sortType)
.sink { result in .sink { result in
switch result { switch result {
case .finished: case .finished:

View File

@ -11,7 +11,7 @@ import Moya
enum SeriesApi { enum SeriesApi {
case getSeriesList(creatorId: Int, sortType: SeriesListAllViewModel.SeriesSortType, page: Int, size: Int) case getSeriesList(creatorId: Int, sortType: SeriesListAllViewModel.SeriesSortType, page: Int, size: Int)
case getSeriesDetail(seriesId: Int) case getSeriesDetail(seriesId: Int)
case getSeriesContentList(seriesId: Int, page: Int, size: Int) case getSeriesContentList(seriesId: Int, page: Int, size: Int, sortType: SeriesListAllViewModel.SeriesSortType)
case getRecommendSeriesList case getRecommendSeriesList
} }
@ -28,7 +28,7 @@ extension SeriesApi: TargetType {
case .getSeriesDetail(let seriesId): case .getSeriesDetail(let seriesId):
return "/audio-content/series/\(seriesId)" return "/audio-content/series/\(seriesId)"
case .getSeriesContentList(let seriesId, _, _): case .getSeriesContentList(let seriesId, _, _, _):
return "/audio-content/series/\(seriesId)/content" return "/audio-content/series/\(seriesId)/content"
case .getRecommendSeriesList: case .getRecommendSeriesList:
@ -58,10 +58,11 @@ extension SeriesApi: TargetType {
case .getSeriesDetail, .getRecommendSeriesList: case .getSeriesDetail, .getRecommendSeriesList:
return .requestPlain return .requestPlain
case .getSeriesContentList(_, let page, let size): case .getSeriesContentList(_, let page, let size, let sortType):
let parameters = [ let parameters = [
"page": page - 1, "page": page - 1,
"size": size "size": size,
"sortType": sortType
] as [String : Any] ] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)

View File

@ -13,7 +13,7 @@ final class SeriesListAllViewModel: ObservableObject {
private var subscription = Set<AnyCancellable>() private var subscription = Set<AnyCancellable>()
enum SeriesSortType: String { enum SeriesSortType: String {
case NEWEST, POPULAR case NEWEST, OLDEST
} }
var creatorId: Int = 0 var creatorId: Int = 0

View File

@ -21,8 +21,8 @@ class SeriesRepository {
return api.requestPublisher(.getSeriesDetail(seriesId: seriesId)) return api.requestPublisher(.getSeriesDetail(seriesId: seriesId))
} }
func getSeriesContentList(seriesId: Int, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> { func getSeriesContentList(seriesId: Int, page: Int, size: Int, sortType: SeriesListAllViewModel.SeriesSortType) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getSeriesContentList(seriesId: seriesId, page: page, size: size)) return api.requestPublisher(.getSeriesContentList(seriesId: seriesId, page: page, size: size, sortType: sortType))
} }
func getRecommendSeriesList() -> AnyPublisher<Response, MoyaError> { func getRecommendSeriesList() -> AnyPublisher<Response, MoyaError> {

View File

@ -189,7 +189,9 @@ struct ContentView: View {
case .tempCanPayment(let orderType, let contentId, let title, let can): case .tempCanPayment(let orderType, let contentId, let title, let can):
CanPaymentTempView(orderType: orderType, contentId: contentId, title: title, can: can) CanPaymentTempView(orderType: orderType, contentId: contentId, title: title, can: can)
case .blockList:
BlockMemberListView()
default: default:
EmptyView() EmptyView()

View File

@ -14,3 +14,4 @@ let AGORA_APP_ID = "b96574e191a9430fa54c605528aa3ef7"
let AGORA_APP_CERTIFICATE = "ae18ade3afcf4086bd4397726eb0654c" let AGORA_APP_CERTIFICATE = "ae18ade3afcf4086bd4397726eb0654c"
let BOOTPAY_APP_ID = "6242a7772701800023f68b2f" let BOOTPAY_APP_ID = "6242a7772701800023f68b2f"
let BOOTPAY_APP_HECTO_ID = "667fca5d3bab7404f831c3e5"

View File

@ -0,0 +1,82 @@
//
// CommunityPostPurchaseDialog.swift
// SodaLive
//
// Created by klaus on 5/24/24.
//
import SwiftUI
struct CommunityPostPurchaseDialog: View {
@Binding var isShowing: Bool
let can: Int
let confirmAction: () -> Void
var body: some View {
GeometryReader { geo in
ZStack {
Color.black
.opacity(0.5)
.frame(width: geo.size.width, height: geo.size.height)
VStack(spacing: 0) {
Text("게시글 보기")
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color.graybb)
.padding(.top, 40)
Text("게시글을\n확인하시겠습니까?")
.font(.custom(Font.medium.rawValue, size: 15))
.foregroundColor(Color.graybb)
.multilineTextAlignment(.center)
.padding(.top, 12)
.padding(.horizontal, 13.3)
HStack(spacing: 13.3) {
Text("취소")
.font(.custom(Font.bold.rawValue, size: 15.3))
.foregroundColor(Color.button)
.padding(.vertical, 16)
.frame(width: (geo.size.width - 66.7) / 3)
.background(Color.bg)
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.button, lineWidth: 1)
)
.onTapGesture {
isShowing = false
}
Text("\(can)캔으로 보기")
.font(.custom(Font.bold.rawValue, size: 15.3))
.foregroundColor(Color.white)
.padding(.vertical, 16)
.frame(width: (geo.size.width - 66.7) * 2 / 3)
.background(Color.button)
.cornerRadius(8)
.onTapGesture {
confirmAction()
isShowing = false
}
}
.padding(.top, 26.7)
.padding(.bottom, 16.7)
}
.frame(width: geo.size.width - 26.7, alignment: .center)
.background(Color.gray22)
.cornerRadius(10)
}
}
}
}
#Preview {
CommunityPostPurchaseDialog(
isShowing: .constant(true),
can: 10,
confirmAction: {}
)
}

View File

@ -0,0 +1,174 @@
//
// MemberProfileDialog.swift
// SodaLive
//
// Created by klaus on 9/7/24.
//
import SwiftUI
import Kingfisher
struct MemberProfileDialog: View {
@StateObject var viewModel = UserViewModel()
@Binding var isShowing: Bool
let memberId: Int
var body: some View {
ZStack {
Color.black.opacity(0.7).ignoresSafeArea()
.onTapGesture {
isShowing = false
}
VStack(alignment: .leading, spacing: 21) {
HStack(spacing: 0) {
Text("프로필")
.font(.custom(Font.bold.rawValue, size: 15))
.foregroundColor(Color.grayee)
Spacer()
Image("ic_close_white")
.onTapGesture {
isShowing = false
}
}
if let profile = viewModel.memberProfile {
Text(profile.nickname)
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color.grayee)
KFImage(URL(string: profile.profileImageUrl))
.resizable()
.frame(maxWidth: screenSize().width - 66.7, maxHeight: screenSize().width - 66.7)
.aspectRatio(CGSize(width: 1, height: 1), contentMode: .fit)
.cornerRadius(8)
HStack(spacing: 8) {
Text(profile.isBlocked ? "차단 해제" : "차단")
.font(.custom(Font.bold.rawValue, size: 15))
.foregroundColor(Color.button)
.frame(maxWidth: .infinity)
.padding(.vertical, 13)
.cornerRadius(8)
.contentShape(Rectangle())
.overlay(
RoundedRectangle(cornerRadius: 8)
.strokeBorder(lineWidth: 1)
.foregroundColor(Color.button)
)
.onTapGesture {
if profile.isBlocked {
viewModel.memberUnBlock()
} else {
viewModel.isShowUesrBlockConfirm = true
}
}
Text("사용자 신고")
.font(.custom(Font.bold.rawValue, size: 15))
.foregroundColor(Color.button)
.frame(maxWidth: .infinity)
.padding(.vertical, 13)
.cornerRadius(8)
.contentShape(Rectangle())
.overlay(
RoundedRectangle(cornerRadius: 8)
.strokeBorder(lineWidth: 1)
.foregroundColor(Color.button)
)
.onTapGesture { viewModel.isShowUesrReportView = true }
Text("프로필 신고")
.font(.custom(Font.bold.rawValue, size: 15))
.foregroundColor(Color.button)
.frame(maxWidth: .infinity)
.padding(.vertical, 13)
.cornerRadius(8)
.contentShape(Rectangle())
.overlay(
RoundedRectangle(cornerRadius: 8)
.strokeBorder(lineWidth: 1)
.foregroundColor(Color.button)
)
.onTapGesture { viewModel.isShowProfileReportConfirm = true }
}
}
}
.padding(.horizontal, 13.3)
.padding(.top, 13.3)
.padding(.bottom, 20)
.background(Color.gray22)
.cornerRadius(8)
.padding(.horizontal, 13.3)
.frame(maxWidth: screenSize().width - 33.3)
.onAppear {
if memberId <= 1 {
viewModel.errorMessage = "잘못된 요청입니다."
viewModel.isShowPopup = true
} else {
viewModel.getMemberProfile(memberId: memberId)
}
}
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
.onDisappear {
if viewModel.dismissDialog {
isShowing = false
}
}
}
if viewModel.isShowUesrBlockConfirm {
UserBlockConfirmDialogView(
isShowing: $viewModel.isShowUesrBlockConfirm,
nickname: viewModel.nickname,
confirmAction: {
viewModel.memberBlock()
}
)
}
if viewModel.isShowUesrReportView {
UserReportDialogView(
isShowing: $viewModel.isShowUesrReportView,
confirmAction: { reason in
viewModel.report(type: .USER, reason: reason)
}
)
}
if viewModel.isShowProfileReportConfirm {
ProfileReportDialogView(
isShowing: $viewModel.isShowProfileReportConfirm,
confirmAction: {
viewModel.report(type: .PROFILE)
}
)
}
if viewModel.isLoading {
LoadingView()
}
}
}
}
#Preview {
MemberProfileDialog(isShowing: .constant(true), memberId: 1)
}

View File

@ -42,13 +42,13 @@ struct SodaDialog: View {
VStack(spacing: 0) { VStack(spacing: 0) {
Text(title) Text(title)
.font(.custom(Font.bold.rawValue, size: 18.3)) .font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "bbbbbb")) .foregroundColor(Color.graybb)
.padding(.top, 40) .padding(.top, 40)
Text(desc) Text(desc)
.font(.custom(Font.medium.rawValue, size: 15)) .font(.custom(Font.medium.rawValue, size: 15))
.foregroundColor(Color(hex: "bbbbbb")) .foregroundColor(Color.graybb)
.multilineTextAlignment(.center) .multilineTextAlignment(.leading)
.padding(.top, 12) .padding(.top, 12)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
@ -57,14 +57,14 @@ struct SodaDialog: View {
if cancelButtonTitle.count > 0 { if cancelButtonTitle.count > 0 {
Text(cancelButtonTitle) Text(cancelButtonTitle)
.font(.custom(Font.bold.rawValue, size: 15.3)) .font(.custom(Font.bold.rawValue, size: 15.3))
.foregroundColor(Color(hex: "3bb9f1")) .foregroundColor(Color.button)
.padding(.vertical, 16) .padding(.vertical, 16)
.frame(width: (geo.size.width - 66.7) / 3) .frame(width: (geo.size.width - 66.7) / 3)
.background(Color(hex: "13181b")) .background(Color.bg)
.cornerRadius(8) .cornerRadius(8)
.overlay( .overlay(
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: 8)
.stroke(Color(hex: "3bb9f1"), lineWidth: 1) .stroke(Color.button, lineWidth: 1)
) )
.onTapGesture { .onTapGesture {
cancelButtonAction() cancelButtonAction()
@ -73,10 +73,10 @@ struct SodaDialog: View {
Text(confirmButtonTitle) Text(confirmButtonTitle)
.font(.custom(Font.bold.rawValue, size: 15.3)) .font(.custom(Font.bold.rawValue, size: 15.3))
.foregroundColor(Color(hex: "ffffff")) .foregroundColor(Color.white)
.padding(.vertical, 16) .padding(.vertical, 16)
.frame(width: (geo.size.width - 66.7) * 2 / 3) .frame(width: (geo.size.width - 66.7) * 2 / 3)
.background(Color(hex: "3bb9f1")) .background(Color.button)
.cornerRadius(8) .cornerRadius(8)
.onTapGesture { .onTapGesture {
confirmButtonAction() confirmButtonAction()
@ -86,7 +86,7 @@ struct SodaDialog: View {
.padding(.bottom, 16.7) .padding(.bottom, 16.7)
} }
.frame(width: geo.size.width - 26.7, alignment: .center) .frame(width: geo.size.width - 26.7, alignment: .center)
.background(Color(hex: "222222")) .background(Color.gray22)
.cornerRadius(10) .cornerRadius(10)
} }
} }

View File

@ -17,6 +17,7 @@ struct CreatorCommunityCommentItemView: View {
let modifyComment: (Int, String) -> Void let modifyComment: (Int, String) -> Void
let onClickDelete: (Int) -> Void let onClickDelete: (Int) -> Void
let onClickProfile: (Int) -> Void
@State var isShowPopupMenu: Bool = false @State var isShowPopupMenu: Bool = false
@State var isModeModify: Bool = false @State var isModeModify: Bool = false
@ -30,6 +31,11 @@ struct CreatorCommunityCommentItemView: View {
.resizable() .resizable()
.frame(width: 40, height: 40) .frame(width: 40, height: 40)
.clipShape(Circle()) .clipShape(Circle())
.onTapGesture {
if UserDefaults.int(forKey: .userId) != commentItem.writerId {
onClickProfile(commentItem.writerId)
}
}
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
Text(commentItem.nickname) Text(commentItem.nickname)
@ -38,7 +44,7 @@ struct CreatorCommunityCommentItemView: View {
Text(commentItem.date) Text(commentItem.date)
.font(.custom(Font.medium.rawValue, size: 10.3)) .font(.custom(Font.medium.rawValue, size: 10.3))
.foregroundColor(Color(hex: "525252")) .foregroundColor(Color.gray52)
.padding(.top, 4) .padding(.top, 4)
} }
@ -57,8 +63,8 @@ struct CreatorCommunityCommentItemView: View {
.autocapitalization(.none) .autocapitalization(.none)
.disableAutocorrection(true) .disableAutocorrection(true)
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color.grayee)
.accentColor(Color(hex: "3bb9f1")) .accentColor(Color.button)
.keyboardType(.default) .keyboardType(.default)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)

View File

@ -17,6 +17,10 @@ struct CreatorCommunityCommentListView: View {
@State private var commentId: Int = 0 @State private var commentId: Int = 0
@State private var isShowDeletePopup: Bool = false @State private var isShowDeletePopup: Bool = false
@State private var memberId: Int = 0
@State private var isShowMemberProfilePopup: Bool = false
@StateObject var viewModel = CreatorCommunityCommentListViewModel() @StateObject var viewModel = CreatorCommunityCommentListViewModel()
var body: some View { var body: some View {
@ -115,6 +119,10 @@ struct CreatorCommunityCommentListView: View {
onClickDelete: { onClickDelete: {
commentId = $0 commentId = $0
isShowDeletePopup = true isShowDeletePopup = true
},
onClickProfile: {
memberId = $0
isShowMemberProfilePopup = true
} }
) )
.padding(.horizontal, 26.7) .padding(.horizontal, 26.7)
@ -147,6 +155,10 @@ struct CreatorCommunityCommentListView: View {
) )
} }
if isShowMemberProfilePopup {
MemberProfileDialog(isShowing: $isShowMemberProfilePopup, memberId: memberId)
}
if viewModel.isLoading { if viewModel.isLoading {
LoadingView() LoadingView()
} }

View File

@ -20,6 +20,9 @@ struct CreatorCommunityCommentReplyView: View {
@State private var commentId: Int = 0 @State private var commentId: Int = 0
@State private var isShowDeletePopup: Bool = false @State private var isShowDeletePopup: Bool = false
@State private var memberId: Int = 0
@State private var isShowMemberProfilePopup: Bool = false
var body: some View { var body: some View {
ZStack { ZStack {
VStack(spacing: 0) { VStack(spacing: 0) {
@ -98,7 +101,11 @@ struct CreatorCommunityCommentReplyView: View {
isReplyComment: true, isReplyComment: true,
isShowPopupMenuButton: false, isShowPopupMenuButton: false,
modifyComment: { _, _ in }, modifyComment: { _, _ in },
onClickDelete: { _ in } onClickDelete: { _ in },
onClickProfile: {
memberId = $0
isShowMemberProfilePopup = true
}
) )
.padding(.horizontal, 26.7) .padding(.horizontal, 26.7)
.padding(.bottom, 13.3) .padding(.bottom, 13.3)
@ -120,6 +127,10 @@ struct CreatorCommunityCommentReplyView: View {
onClickDelete: { onClickDelete: {
commentId = $0 commentId = $0
isShowDeletePopup = true isShowDeletePopup = true
},
onClickProfile: {
memberId = $0
isShowMemberProfilePopup = true
} }
) )
.padding(.horizontal, 26.7) .padding(.horizontal, 26.7)
@ -154,6 +165,10 @@ struct CreatorCommunityCommentReplyView: View {
) )
} }
if isShowMemberProfilePopup {
MemberProfileDialog(isShowing: $isShowMemberProfilePopup, memberId: memberId)
}
if viewModel.isLoading { if viewModel.isLoading {
LoadingView() LoadingView()
} }

View File

@ -0,0 +1,39 @@
//
// CreatorCommunityAllItemLockView.swift
// SodaLive
//
// Created by klaus on 5/24/24.
//
import SwiftUI
struct CreatorCommunityAllItemLockView: View {
let price: Int
let onClickPurchaseContent: () -> Void
var body: some View {
VStack(spacing: 26.7) {
Image("ic_lock_bb")
Text("\(price)캔으로 게시글 보기")
.font(.custom(Font.bold.rawValue, size: 12))
.foregroundColor(Color.button)
.padding(.horizontal, 21)
.padding(.vertical, 11)
.overlay(
RoundedRectangle(cornerRadius: 26.7)
.stroke(Color.button, lineWidth: 1)
)
.onTapGesture { onClickPurchaseContent() }
}
.frame(width: screenSize().width - 42, height: screenSize().width - 42)
.background(Color.gray33)
.cornerRadius(5.3)
}
}
#Preview {
CreatorCommunityAllItemLockView(price: 100) {
}
}

View File

@ -15,23 +15,29 @@ struct CreatorCommunityAllItemView: View {
let onClickComment: () -> Void let onClickComment: () -> Void
let onClickWriteComment: (String) -> Void let onClickWriteComment: (String) -> Void
let onClickShowReportMenu: () -> Void let onClickShowReportMenu: () -> Void
let onClickPurchaseContent: () -> Void
@State var isLike = false @State var isLike = false
@State var likeCount = 0 @State var likeCount = 0
@State private var textHeight: CGFloat = .zero @State private var textHeight: CGFloat = .zero
@StateObject var playManager = CreatorCommunityMediaPlayerManager.shared
@StateObject var contentPlayManager = ContentPlayManager.shared
init( init(
item: GetCommunityPostListResponse, item: GetCommunityPostListResponse,
onClickLike: @escaping () -> Void, onClickLike: @escaping () -> Void,
onClickComment: @escaping () -> Void, onClickComment: @escaping () -> Void,
onClickWriteComment: @escaping (String) -> Void, onClickWriteComment: @escaping (String) -> Void,
onClickShowReportMenu: @escaping () -> Void onClickShowReportMenu: @escaping () -> Void,
onClickPurchaseContent: @escaping () -> Void
) { ) {
self.item = item self.item = item
self.onClickLike = onClickLike self.onClickLike = onClickLike
self.onClickComment = onClickComment self.onClickComment = onClickComment
self.onClickWriteComment = onClickWriteComment self.onClickWriteComment = onClickWriteComment
self.onClickShowReportMenu = onClickShowReportMenu self.onClickShowReportMenu = onClickShowReportMenu
self.onClickPurchaseContent = onClickPurchaseContent
self._isLike = State(initialValue: item.isLike) self._isLike = State(initialValue: item.isLike)
self._likeCount = State(initialValue: item.likeCount) self._likeCount = State(initialValue: item.likeCount)
@ -58,72 +64,91 @@ struct CreatorCommunityAllItemView: View {
Spacer() Spacer()
Image("ic_seemore_vertical") if item.price <= 0 || item.existOrdered {
.padding(.trailing, 8.3) Image("ic_seemore_vertical")
.onTapGesture { onClickShowReportMenu() } .padding(.trailing, 8.3)
.onTapGesture { onClickShowReportMenu() }
}
} }
DetectableTextView(text: item.content, textSize: 13.3, font: Font.medium.rawValue) DetectableTextView(text: item.content, textSize: 13.3, font: Font.medium.rawValue)
.frame( .frame(
width: screenSize().width - 16, width: screenSize().width - 42,
height: textHeight height: textHeight
) )
.onAppear { .onAppear {
self.textHeight = self.estimatedHeight( self.textHeight = self.estimatedHeight(
for: item.content, for: item.content,
width: screenSize().width - 16 width: screenSize().width - 42
) )
} }
.onChange(of: item.content) { newText in .onChange(of: item.content) { newText in
self.textHeight = self.estimatedHeight( self.textHeight = self.estimatedHeight(
for: newText, for: newText,
width: screenSize().width - 16 width: screenSize().width - 42
) )
} }
if let imageUrl = item.imageUrl { if item.price <= 0 || item.existOrdered {
KFImage(URL(string: imageUrl)) if let imageUrl = item.imageUrl {
.resizable() ZStack {
.frame(maxWidth: .infinity) KFImage(URL(string: imageUrl))
.scaledToFit() .resizable()
} .frame(maxWidth: .infinity)
.scaledToFit()
HStack(spacing: 8) {
IconAndTitleToggleButton( if let audioUrl = item.audioUrl {
isChecked: isLike, Image(playManager.isPlaying && playManager.currentPlayingContentId == item.postId ? "btn_audio_content_pause" : "btn_audio_content_play")
title: "\(likeCount)", .onTapGesture {
normalIconName: "ic_audio_content_heart_normal", contentPlayManager.pauseAudio()
checkedIconName: "ic_audio_content_heart_pressed" playManager.toggleContent(item: CreatorCommunityContentItem(contentId: item.postId, url: audioUrl))
) { }
if isLike { }
isLike = false
likeCount -= 1
} else {
isLike = true
likeCount += 1
}
onClickLike()
}
}
.frame(maxWidth: .infinity, alignment: .leading)
if item.isCommentAvailable {
CreatorCommunityCommentView(
commentCount: item.commentCount,
commentItem: item.firstComment,
onClickWriteComment: onClickWriteComment
)
.onTapGesture {
if item.commentCount > 0 {
onClickComment()
} }
} }
HStack(spacing: 8) {
IconAndTitleToggleButton(
isChecked: isLike,
title: "\(likeCount)",
normalIconName: "ic_audio_content_heart_normal",
checkedIconName: "ic_audio_content_heart_pressed"
) {
if isLike {
isLike = false
likeCount -= 1
} else {
isLike = true
likeCount += 1
}
onClickLike()
}
}
.frame(maxWidth: .infinity, alignment: .leading)
if item.isCommentAvailable {
CreatorCommunityCommentView(
commentCount: item.commentCount,
commentItem: item.firstComment,
onClickWriteComment: onClickWriteComment
)
.onTapGesture {
if item.commentCount > 0 {
onClickComment()
}
}
}
} else {
CreatorCommunityAllItemLockView(
price: item.price,
onClickPurchaseContent: onClickPurchaseContent)
} }
} }
.padding(.horizontal, 8) .padding(.horizontal, 8)
.padding(.vertical, 11) .padding(.vertical, 11)
.background(Color.gray22) .background(Color.gray22)
.cornerRadius(5.3) .cornerRadius(5.3)
.padding(.horizontal, 13.3)
} }
private func estimatedHeight(for text: String, width: CGFloat) -> CGFloat { private func estimatedHeight(for text: String, width: CGFloat) -> CGFloat {
@ -143,11 +168,14 @@ struct CreatorCommunityAllItemView_Previews: PreviewProvider {
creatorNickname: "민하나", creatorNickname: "민하나",
creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png", creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
audioUrl: nil,
content: "너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!", content: "너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!",
price: 10,
date: "3일전", date: "3일전",
isCommentAvailable: false, isCommentAvailable: false,
isAdult: false, isAdult: false,
isLike: true, isLike: true,
existOrdered: false,
likeCount: 10, likeCount: 10,
commentCount: 0, commentCount: 0,
firstComment: nil firstComment: nil
@ -155,7 +183,8 @@ struct CreatorCommunityAllItemView_Previews: PreviewProvider {
onClickLike: {}, onClickLike: {},
onClickComment: {}, onClickComment: {},
onClickWriteComment: { _ in }, onClickWriteComment: { _ in },
onClickShowReportMenu: {} onClickShowReportMenu: {},
onClickPurchaseContent: {}
) )
} }
} }

View File

@ -12,6 +12,7 @@ struct CreatorCommunityAllView: View {
let creatorId: Int let creatorId: Int
@StateObject var viewModel = CreatorCommunityAllViewModel() @StateObject var viewModel = CreatorCommunityAllViewModel()
@StateObject var playerManager = CreatorCommunityMediaPlayerManager.shared
var body: some View { var body: some View {
GeometryReader { proxy in GeometryReader { proxy in
@ -41,6 +42,12 @@ struct CreatorCommunityAllView: View {
onClickShowReportMenu: { onClickShowReportMenu: {
viewModel.postId = item.postId viewModel.postId = item.postId
viewModel.isShowReportMenu = true viewModel.isShowReportMenu = true
},
onClickPurchaseContent: {
viewModel.postId = item.postId
viewModel.postPrice = item.price
viewModel.postIndex = index
viewModel.isShowPostPurchaseView = true
} }
) )
.onAppear { .onAppear {
@ -50,7 +57,6 @@ struct CreatorCommunityAllView: View {
} }
} }
} }
.padding(5.3)
} }
} }
.sheet( .sheet(
@ -121,6 +127,19 @@ struct CreatorCommunityAllView: View {
} }
) )
} }
if viewModel.isShowPostPurchaseView {
CommunityPostPurchaseDialog(
isShowing: $viewModel.isShowPostPurchaseView,
can: viewModel.postPrice
) {
viewModel.purchaseCommunityPost()
}
}
if playerManager.isLoading {
LoadingView()
}
} }
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
@ -131,7 +150,24 @@ struct CreatorCommunityAllView: View {
.padding(.vertical, 13.3) .padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center) .frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12)) .font(.custom(Font.medium.rawValue, size: 12))
.background(Color(hex: "9970ff")) .background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
}
}
.popup(isPresented: $playerManager.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
GeometryReader { geo in
HStack {
Spacer()
Text(playerManager.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color.button)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.cornerRadius(20) .cornerRadius(20)
@ -144,6 +180,9 @@ struct CreatorCommunityAllView: View {
viewModel.creatorId = creatorId viewModel.creatorId = creatorId
viewModel.getCommunityPostList() viewModel.getCommunityPostList()
} }
.onDisappear {
CreatorCommunityMediaPlayerManager.shared.stopContent()
}
} }
} }

View File

@ -21,6 +21,8 @@ class CreatorCommunityAllViewModel: ObservableObject {
@Published private(set) var communityPostList = [GetCommunityPostListResponse]() @Published private(set) var communityPostList = [GetCommunityPostListResponse]()
@Published var postId = 0 @Published var postId = 0
@Published var postPrice = 0
@Published var postIndex = -1
@Published var isShowCommentListView = false { @Published var isShowCommentListView = false {
didSet { didSet {
@ -46,6 +48,16 @@ class CreatorCommunityAllViewModel: ObservableObject {
} }
} }
@Published var isShowPostPurchaseView = false {
didSet {
if !isShowPostPurchaseView {
postId = 0
postPrice = 0
postIndex = -1
}
}
}
var creatorId = 0 var creatorId = 0
var page = 1 var page = 1
@ -254,4 +266,51 @@ class CreatorCommunityAllViewModel: ObservableObject {
self.isLoading = false self.isLoading = false
} }
} }
func purchaseCommunityPost() {
let postId = postId
let postIndex = postIndex
if !isLoading {
isLoading = true
repository
.purchaseCommunityPost(postId: postId)
.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<GetCommunityPostListResponse>.self, from: responseData)
if let data = decoded.data, decoded.success {
if postIndex >= 0 {
communityPostList[postIndex] = data
}
} 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,130 @@
//
// CreatorCommunityMediaPlayerManager.swift
// SodaLive
//
// Created by klaus on 8/7/24.
//
import Foundation
import AVKit
import MediaPlayer
final class CreatorCommunityMediaPlayerManager: NSObject, ObservableObject {
static let shared = CreatorCommunityMediaPlayerManager()
@Published private (set) var currentPlayingContentId: Int = 0
@Published private (set) var isPlaying = false
@Published var isLoading = false
@Published var errorMessage = ""
@Published var isShowPopup = false
private var player: AVAudioPlayer!
private func playContent(item: CreatorCommunityContentItem) {
if item.contentId <= 0 {
return
}
if (currentPlayingContentId == item.contentId && !isPlaying) {
resumeContent()
return
}
stopContent()
currentPlayingContentId = item.contentId
guard let url = URL(string: item.url) else {
showError()
return
}
isLoading = true
URLSession.shared.dataTask(with: url) { [unowned self] data, response, error in
guard let audioData = data else {
self.isLoading = false
return
}
do {
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .moviePlayback)
try audioSession.setActive(true)
self.player = try AVAudioPlayer(data: audioData)
DispatchQueue.main.async {
self.player?.volume = 1
self.player?.delegate = self
self.player?.prepareToPlay()
self.player?.play()
self.isPlaying = self.player.isPlaying
}
} catch {
DispatchQueue.main.async {
self.showError()
}
}
DispatchQueue.main.async {
self.isLoading = false
}
}.resume()
}
private func resumeContent() {
if let player = player {
player.play()
isPlaying = player.isPlaying
}
}
private func showError() {
self.errorMessage = "오류가 발생했습니다. 다시 시도해 주세요."
self.isShowPopup = true
}
}
extension CreatorCommunityMediaPlayerManager {
func toggleContent(item: CreatorCommunityContentItem) {
if currentPlayingContentId == item.contentId {
if let player = player, player.isPlaying {
pauseContent()
} else {
resumeContent()
}
} else {
playContent(item: item)
}
}
func pauseContent() {
if let player = player {
player.pause()
isPlaying = player.isPlaying
}
}
func stopContent() {
if let player = player {
player.stop()
player.currentTime = 0
}
isPlaying = false
currentPlayingContentId = 0
}
}
extension CreatorCommunityMediaPlayerManager: AVAudioPlayerDelegate {
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
stopContent()
}
}
struct CreatorCommunityContentItem {
let contentId: Int
let url: String
}

View File

@ -0,0 +1,14 @@
//
// PurchasePostRequest.swift
// SodaLive
//
// Created by klaus on 5/24/24.
//
import Foundation
struct PurchasePostRequest: Encodable {
let postId: Int
let container: String = "ios"
let timezone: String = TimeZone.current.identifier
}

View File

@ -19,6 +19,7 @@ enum CreatorCommunityApi {
case getCommentReplyList(commentId: Int, page: Int, size: Int) case getCommentReplyList(commentId: Int, page: Int, size: Int)
case modifyComment(request: ModifyCommunityPostCommentRequest) case modifyComment(request: ModifyCommunityPostCommentRequest)
case getLatestPostListFromCreatorsYouFollow case getLatestPostListFromCreatorsYouFollow
case purchaseCommunityPost(postId: Int)
} }
extension CreatorCommunityApi: TargetType { extension CreatorCommunityApi: TargetType {
@ -48,12 +49,15 @@ extension CreatorCommunityApi: TargetType {
case .getLatestPostListFromCreatorsYouFollow: case .getLatestPostListFromCreatorsYouFollow:
return "/creator-community/latest" return "/creator-community/latest"
case .purchaseCommunityPost:
return "/creator-community/purchase"
} }
} }
var method: Moya.Method { var method: Moya.Method {
switch self { switch self {
case .createCommunityPost, .communityPostLike, .createCommunityPostComment: case .createCommunityPost, .communityPostLike, .createCommunityPostComment, .purchaseCommunityPost:
return .post return .post
case .getCommunityPostList, .getCommunityPostCommentList, .getCommentReplyList, .getCommunityPostDetail, .getLatestPostListFromCreatorsYouFollow: case .getCommunityPostList, .getCommunityPostCommentList, .getCommentReplyList, .getCommunityPostDetail, .getLatestPostListFromCreatorsYouFollow:
@ -115,6 +119,9 @@ extension CreatorCommunityApi: TargetType {
case .getLatestPostListFromCreatorsYouFollow: case .getLatestPostListFromCreatorsYouFollow:
let parameters = ["timezone": TimeZone.current.identifier] as [String: Any] let parameters = ["timezone": TimeZone.current.identifier] as [String: Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .purchaseCommunityPost(let postId):
return .requestJSONEncodable(PurchasePostRequest(postId: postId))
} }
} }

View File

@ -22,19 +22,19 @@ struct CreatorCommunityItemView: View {
Text(item.creatorNickname) Text(item.creatorNickname)
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color.grayee)
Spacer() Spacer()
Text(item.date) Text(item.date)
.font(.custom(Font.light.rawValue, size: 13.3)) .font(.custom(Font.light.rawValue, size: 13.3))
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color.gray77)
} }
HStack(spacing: 0) { HStack(spacing: 0) {
Text(item.content) Text(item.content)
.font(.custom(Font.medium.rawValue, size: 12)) .font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "bbbbbb")) .foregroundColor(Color.graybb)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.lineLimit(3) .lineLimit(3)
@ -45,9 +45,10 @@ struct CreatorCommunityItemView: View {
.resizable() .resizable()
.frame(width: 53.3, height: 53.3) .frame(width: 53.3, height: 53.3)
.cornerRadius(4.7) .cornerRadius(4.7)
.blur(radius: item.existOrdered || item.price <= 0 ? 0 : 15)
} else { } else {
Rectangle() Rectangle()
.foregroundColor(Color(hex: "222222").opacity(0)) .foregroundColor(Color.gray22.opacity(0))
.frame(width: 53.3, height: 53.3) .frame(width: 53.3, height: 53.3)
} }
} }
@ -60,7 +61,7 @@ struct CreatorCommunityItemView: View {
Text("\(item.likeCount)") Text("\(item.likeCount)")
.font(.custom(Font.medium.rawValue, size: 11)) .font(.custom(Font.medium.rawValue, size: 11))
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color.gray77)
} }
HStack(spacing: 6) { HStack(spacing: 6) {
@ -70,13 +71,13 @@ struct CreatorCommunityItemView: View {
Text("\(item.commentCount)") Text("\(item.commentCount)")
.font(.custom(Font.medium.rawValue, size: 11)) .font(.custom(Font.medium.rawValue, size: 11))
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color.gray77)
} }
} }
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(13.3) .padding(13.3)
.background(Color(hex: "222222")) .background(Color.gray22)
.cornerRadius(11) .cornerRadius(11)
} }
} }
@ -90,11 +91,14 @@ struct CreatorCommunityItemView_Previews: PreviewProvider {
creatorNickname: "민하나", creatorNickname: "민하나",
creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png", creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
audioUrl: nil,
content: "안녕하세요", content: "안녕하세요",
price: 10,
date: "3일전", date: "3일전",
isCommentAvailable: false, isCommentAvailable: false,
isAdult: false, isAdult: false,
isLike: false, isLike: false,
existOrdered: false,
likeCount: 10, likeCount: 10,
commentCount: 0, commentCount: 0,
firstComment: nil firstComment: nil

View File

@ -52,4 +52,8 @@ class CreatorCommunityRepository {
func getLatestPostListFromCreatorsYouFollow() -> AnyPublisher<Response, MoyaError> { func getLatestPostListFromCreatorsYouFollow() -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getLatestPostListFromCreatorsYouFollow) return api.requestPublisher(.getLatestPostListFromCreatorsYouFollow)
} }
func purchaseCommunityPost(postId: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.purchaseCommunityPost(postId: postId))
}
} }

View File

@ -11,11 +11,14 @@ struct GetCommunityPostListResponse: Decodable {
let creatorNickname: String let creatorNickname: String
let creatorProfileUrl: String let creatorProfileUrl: String
let imageUrl: String? let imageUrl: String?
let audioUrl: String?
let content: String let content: String
let price: Int
let date: String let date: String
let isCommentAvailable: Bool let isCommentAvailable: Bool
let isAdult: Bool let isAdult: Bool
let isLike: Bool let isLike: Bool
let existOrdered: Bool
let likeCount: Int let likeCount: Int
let commentCount: Int let commentCount: Int
let firstComment: GetCommunityPostCommentListItem? let firstComment: GetCommunityPostCommentListItem?

View File

@ -9,6 +9,7 @@ import Foundation
struct CreateCommunityPostRequest: Encodable { struct CreateCommunityPostRequest: Encodable {
let content: String let content: String
let price: Int
let isAdult: Bool let isAdult: Bool
let isCommentAvailable: Bool let isCommentAvailable: Bool
} }

View File

@ -0,0 +1,210 @@
//
// CreatorCommunityRecordingVoiceView.swift
// SodaLive
//
// Created by klaus on 8/7/24.
//
import SwiftUI
struct CreatorCommunityRecordingVoiceView: View {
@StateObject var soundManager = CreatorCommunitySoundManager()
@Binding var isShowing: Bool
@Binding var isShowPopup: Bool
@Binding var errorMessage: String
@Binding var fileName: String
@Binding var soundData: Data?
@State private var tempFileName = ""
var body: some View {
ZStack {
Color.black.opacity(0.7)
.ignoresSafeArea()
GeometryReader { proxy in
VStack {
Spacer()
VStack {
VStack(spacing: 0) {
HStack(spacing: 0) {
Text("음성녹음")
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(.white)
Spacer()
Image("ic_close_white")
.resizable()
.frame(width: 20, height: 20)
.onTapGesture { isShowing = false }
}
.padding(.horizontal, 26.7)
.padding(.top, 26.7)
}
Text(soundManager.timeString)
.font(.custom(Font.bold.rawValue, size: 33.3))
.foregroundColor(.white)
.padding(.top, 80)
switch soundManager.recordMode {
case .RECORD:
if !soundManager.isLoading {
Image(soundManager.isRecording ? "ic_record_stop" : "ic_record")
.resizable()
.frame(width: 70, height: 70)
.padding(.vertical, 52.3)
.onTapGesture {
if !soundManager.isLoading {
if !soundManager.isRecording {
tempFileName = "now_voice_\(Int(Date().timeIntervalSince1970 * 1000)).m4a"
soundManager.startRecording(tempFileName)
} else {
soundManager.stopRecording()
soundManager.recordMode = .PLAY
}
}
}
}
case .PLAY:
if !soundManager.isLoading {
VStack(spacing: 0) {
HStack(spacing: 0) {
Spacer()
Text("삭제")
.font(.custom(Font.medium.rawValue, size: 15.3))
.foregroundColor(Color.graybb.opacity(0))
Spacer()
Image(
!soundManager.isPlaying ?
"ic_record_play" :
"ic_record_pause"
)
.onTapGesture {
if !soundManager.isLoading {
if !soundManager.isPlaying {
soundManager.playAudio()
} else {
soundManager.stopAudio()
}
}
}
Spacer()
Text("삭제")
.font(.custom(Font.medium.rawValue, size: 15.3))
.foregroundColor(Color.graybb)
.onTapGesture {
soundManager.stopAudio()
soundManager.deleteAudioFile()
soundManager.recordMode = .RECORD
}
Spacer()
}
.padding(.vertical, 52.3)
HStack(spacing: 13.3) {
Text("다시 녹음")
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color.button)
.frame(width: (proxy.size.width - 40) / 3, height: 50)
.background(Color.button.opacity(0.2))
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.button, lineWidth: 1.3)
)
.onTapGesture {
soundManager.stopAudio()
soundManager.deleteAudioFile()
soundManager.recordMode = .RECORD
}
Text("녹음완료")
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(.white)
.frame(width: (proxy.size.width - 40) * 2 / 3, height: 50)
.background(Color.button)
.cornerRadius(10)
.onTapGesture {
do {
let soundData = try Data(contentsOf: soundManager.getAudioFileURL())
self.soundData = soundData
self.fileName = tempFileName
self.isShowing = false
} catch {
errorMessage = "녹음파일을 생성하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
isShowPopup = true
}
}
}
.padding(.bottom, 40)
.padding(.horizontal, 13.3)
}
}
}
if proxy.safeAreaInsets.bottom > 0 {
Rectangle()
.foregroundColor(Color.gray22)
.frame(width: proxy.size.width, height: 15.3)
}
if soundManager.isLoading {
LoadingView()
}
}
.background(Color(hex: "222222"))
.cornerRadius(16.7, corners: [.topLeft, .topRight])
}
.edgesIgnoringSafeArea(.bottom)
.onAppear {
soundManager.prepareRecording()
}
}
}
.popup(isPresented: $soundManager.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
GeometryReader { geo in
HStack {
Spacer()
Text(soundManager.errorMessage)
.padding(.vertical, 13.3)
.padding(.horizontal, 6.7)
.frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
.onDisappear {
if soundManager.onClose {
isShowing = false
}
}
}
}
}
}
#Preview {
CreatorCommunityRecordingVoiceView(
isShowing: .constant(false),
isShowPopup: .constant(false),
errorMessage: .constant(""),
fileName: .constant(""),
soundData: .constant(nil)
)
}

View File

@ -0,0 +1,205 @@
//
// CreatorCommunitySoundManager.swift
// SodaLive
//
// Created by klaus on 8/7/24.
//
import Foundation
import AVKit
import Combine
class CreatorCommunitySoundManager: NSObject, ObservableObject {
enum RecordMode {
case RECORD, PLAY
}
@Published var recordMode = RecordMode.RECORD
@Published var errorMessage = ""
@Published var isShowPopup = false
@Published var isLoading = false
@Published var onClose = false
@Published var isPlaying = false
@Published var isRecording = false
@Published var timeString = "00:00.00"
private var timerSubscription: Cancellable?
private var startTime: Date?
var player: AVAudioPlayer!
var audioRecorder: AVAudioRecorder!
var fileName = "now_voice.m4a"
let audioSession = AVAudioSession.sharedInstance()
func prepareRecording() {
isLoading = true
do {
try audioSession.setCategory(.playAndRecord, mode: .videoRecording)
try audioSession.setActive(true)
audioSession.requestRecordPermission() { [weak self] allowed in
DispatchQueue.main.async {
if !allowed {
self?.errorMessage = "권한을 허용하지 않으시면 음성녹음을 하실 수 없습니다."
self?.isShowPopup = true
self?.onClose = true
}
}
}
} catch {
errorMessage = "오류가 발생했습니다. 다시 시도해 주세요."
isShowPopup = true
onClose = true
}
isLoading = false
}
func startRecording(_ fileName: String) {
self.fileName = fileName
player?.stop()
player = nil
isLoading = true
let settings = [
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
AVSampleRateKey: 48000,
AVEncoderBitRateKey: 256000,
AVNumberOfChannelsKey: 2,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
]
do {
try audioSession.setCategory(.playAndRecord, mode: .videoRecording)
try audioSession.setActive(true)
audioRecorder = try AVAudioRecorder(url: getAudioFileURL(), settings: settings)
audioRecorder.record()
isRecording = true
startTimer()
} catch {
errorMessage = "오류가 발생했습니다. 다시 시도해 주세요."
isShowPopup = true
}
isLoading = false
}
func stopRecording() {
stopTimer()
audioRecorder?.stop()
audioRecorder = nil
isRecording = false
prepareForPlay()
}
func prepareForPlay(_ url: URL? = nil) {
isLoading = true
DispatchQueue.main.async {
do {
try self.audioSession.setCategory(.playback, mode: .moviePlayback)
try self.audioSession.setActive(true)
if let url = url {
self.player = try AVAudioPlayer(data: Data(contentsOf: url))
} else {
self.player = try AVAudioPlayer(contentsOf: self.getAudioFileURL())
}
self.player?.volume = 1
self.player?.delegate = self
self.player?.prepareToPlay()
} catch {
self.errorMessage = "오류가 발생했습니다. 다시 시도해 주세요."
self.isShowPopup = true
}
self.isLoading = false
}
}
func playAudio() {
player?.play()
isPlaying = player.isPlaying
startTimer()
}
func stopAudio() {
stopTimer()
player?.stop()
player.currentTime = 0
isPlaying = player.isPlaying
}
func deleteAudioFile() {
do {
try FileManager.default.removeItem(at: getAudioFileURL())
} catch {}
}
func getAudioFileURL() -> URL {
return getDocumentsDirectory().appendingPathComponent(fileName)
}
private func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
func startTimer() {
timeString = "00:00.00"
startTime = Date()
timerSubscription = Timer.publish(every: 0.01, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
self?.updateTime()
}
}
func stopTimer() {
timeString = "00:00.00"
timerSubscription?.cancel()
startTime = nil
timerSubscription = nil
}
private func updateTime() {
guard let startTime = startTime else { return }
let elapsedTime = Date().timeIntervalSince(startTime)
let minutes = Int(elapsedTime) / 60
let seconds = Int(elapsedTime) % 60
let centiseconds = Int((elapsedTime - Double(minutes * 60) - Double(seconds)) * 100)
timeString = String(format: "%02d:%02d.%02d", minutes, seconds, centiseconds)
if minutes >= 3 {
stopRecording()
recordMode = .PLAY
}
}
deinit {
player?.stop()
audioRecorder?.stop()
startTime = nil
timerSubscription?.cancel()
}
}
extension CreatorCommunitySoundManager: AVAudioPlayerDelegate {
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
stopAudio()
}
}

View File

@ -12,7 +12,9 @@ struct CreatorCommunityWriteView: View {
@StateObject var keyboardHandler = KeyboardHandler() @StateObject var keyboardHandler = KeyboardHandler()
@StateObject private var viewModel = CreatorCommunityWriteViewModel() @StateObject private var viewModel = CreatorCommunityWriteViewModel()
@State private var isShowRecordingVoiceView = false
@State private var isShowPhotoPicker = false @State private var isShowPhotoPicker = false
@State private var fileName: String = "녹음"
let onSuccess: () -> Void let onSuccess: () -> Void
var body: some View { var body: some View {
@ -27,7 +29,7 @@ struct CreatorCommunityWriteView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
Text("이미지") Text("이미지")
.font(.custom(Font.bold.rawValue, size: 16.7)) .font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color.grayee)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
ZStack { ZStack {
@ -45,14 +47,14 @@ struct CreatorCommunityWriteView: View {
.scaledToFit() .scaledToFit()
.padding(13.3) .padding(13.3)
.frame(width: 107, height: 107) .frame(width: 107, height: 107)
.background(Color(hex: "13181B")) .background(Color.bg)
.cornerRadius(8) .cornerRadius(8)
.clipped() .clipped()
} }
Image("ic_camera") Image("ic_camera")
.padding(10) .padding(10)
.background(Color(hex: "3BB9F1")) .background(Color.button)
.cornerRadius(30) .cornerRadius(30)
.offset(x: 50, y: 36) .offset(x: 50, y: 36)
} }
@ -63,28 +65,60 @@ struct CreatorCommunityWriteView: View {
HStack(alignment: .top, spacing: 0) { HStack(alignment: .top, spacing: 0) {
Text("") Text("")
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color.gray77)
Text("등록할 이미지가 없으면 이미지 없이 게시글만 등록 하셔도 됩니다.") Text("등록할 이미지가 없으면 이미지 없이 게시글만 등록 하셔도 됩니다.")
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color.gray77)
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity)
.padding(.top, 24) .padding(.top, 24)
if let _ = viewModel.postImage {
VStack(spacing: 13.3) {
HStack(spacing: 0) {
Text("오디오 녹음")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color.grayee)
Spacer()
}
Text(fileName)
.font(.custom(Font.medium.rawValue, size: 16.7))
.foregroundColor(Color.main)
.padding(.vertical, 8)
.background(Color.bg)
.cornerRadius(5.3)
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
.overlay(
RoundedRectangle(cornerRadius: 5.3)
.stroke(Color.button, lineWidth: 1)
)
.onTapGesture { isShowRecordingVoiceView = true }
Text("※ 오디오 녹음은 최대 3분입니다")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color.gray77)
}
.padding(.top, 24)
}
HStack(spacing: 0) { HStack(spacing: 0) {
Text("내용") Text("내용")
.font(.custom(Font.bold.rawValue, size: 16.7)) .font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color.grayee)
Spacer() Spacer()
Text("\(viewModel.content.count)") Text("\(viewModel.content.count)")
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "ff5c49")) + .foregroundColor(Color.mainRed) +
Text(" / 최대 500자") Text(" / 최대 500자")
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color.gray77)
} }
.padding(.top, 26.7) .padding(.top, 26.7)
@ -101,7 +135,7 @@ struct CreatorCommunityWriteView: View {
VStack(spacing: 13.3) { VStack(spacing: 13.3) {
Text("댓글 가능 여부") Text("댓글 가능 여부")
.font(.custom(Font.bold.rawValue, size: 16.7)) .font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color.grayee)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 13.3) { HStack(spacing: 13.3) {
@ -126,33 +160,92 @@ struct CreatorCommunityWriteView: View {
} }
.padding(.top, 26.7) .padding(.top, 26.7)
VStack(spacing: 13.3) { if UserDefaults.bool(forKey: .auth) {
Text("연령 제한") VStack(spacing: 13.3) {
.font(.custom(Font.bold.rawValue, size: 16.7)) Text("연령 제한")
.foregroundColor(Color(hex: "eeeeee")) .font(.custom(Font.bold.rawValue, size: 16.7))
.frame(maxWidth: .infinity, alignment: .leading) .foregroundColor(Color.grayee)
.frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 13.3) {
SelectButtonView(
title: "전체 연령",
isChecked: !viewModel.isAdult
) {
if viewModel.isAdult {
viewModel.isAdult = false
}
}
SelectButtonView( HStack(spacing: 13.3) {
title: "19세 이상", SelectButtonView(
isChecked: viewModel.isAdult title: "전체 연령",
) { isChecked: !viewModel.isAdult
if !viewModel.isAdult { ) {
viewModel.isAdult = true if viewModel.isAdult {
viewModel.isAdult = false
}
}
SelectButtonView(
title: "19세 이상",
isChecked: viewModel.isAdult
) {
if !viewModel.isAdult {
viewModel.isAdult = true
}
} }
} }
} }
.padding(.top, 26.7)
}
if let _ = viewModel.postImage {
VStack(spacing: 13.3) {
Text("가격 설정")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color.grayee)
.frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 13.3) {
SelectButtonView(
title: "무료",
isChecked: viewModel.isPriceFree
) {
if !viewModel.isPriceFree {
viewModel.isPriceFree = true
}
}
SelectButtonView(
title: "유료",
isChecked: !viewModel.isPriceFree
) {
if viewModel.isPriceFree {
viewModel.isPriceFree = false
}
}
}
if !viewModel.isPriceFree {
HStack(spacing: 0) {
TextField("", text: $viewModel.priceString)
.autocapitalization(.none)
.disableAutocorrection(true)
.multilineTextAlignment(.center)
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color.button)
.accentColor(Color.button)
.keyboardType(.numberPad)
Spacer()
Text("")
.font(.custom(Font.medium.rawValue, size: 14.7))
.foregroundColor(Color.button)
}
.padding(.horizontal, 13.3)
.padding(.vertical, 16.7)
.frame(maxWidth: .infinity)
.overlay(
RoundedRectangle(cornerRadius: 6.7)
.stroke(Color.gray77, lineWidth: 1)
)
.background(Color.gray23)
}
}
.padding(.top, 26.7)
} }
.padding(.top, 26.7)
} }
.padding(13.3) .padding(13.3)
@ -160,14 +253,14 @@ struct CreatorCommunityWriteView: View {
HStack(spacing: 13.3) { HStack(spacing: 13.3) {
Text("닫기") Text("닫기")
.font(.custom(Font.bold.rawValue, size: 18.3)) .font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "3BB9F1")) .foregroundColor(Color.button)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.frame(height: 50) .frame(height: 50)
.background(Color(hex: "13181B")) .background(Color.bg)
.cornerRadius(10) .cornerRadius(10)
.overlay( .overlay(
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: 8)
.stroke(Color(hex: "3BB9F1"), lineWidth: 1) .stroke(Color.button, lineWidth: 1)
) )
.onTapGesture { .onTapGesture {
hideKeyboard() hideKeyboard()
@ -179,11 +272,12 @@ struct CreatorCommunityWriteView: View {
.foregroundColor(Color.white) .foregroundColor(Color.white)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.frame(height: 50) .frame(height: 50)
.background(Color(hex: "3BB9F1")) .background(Color.button)
.cornerRadius(10) .cornerRadius(10)
.onTapGesture { .onTapGesture {
hideKeyboard() hideKeyboard()
viewModel.createCommunityPost { viewModel.createCommunityPost {
deleteAudioFile()
AppState.shared.back() AppState.shared.back()
DispatchQueue.main.async { DispatchQueue.main.async {
@ -194,17 +288,17 @@ struct CreatorCommunityWriteView: View {
} }
.padding(13.3) .padding(13.3)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.background(Color(hex: "222222")) .background(Color.gray22)
.cornerRadius(16.7, corners: [.topLeft, .topRight]) .cornerRadius(16.7, corners: [.topLeft, .topRight])
Rectangle() Rectangle()
.foregroundColor(Color(hex: "222222")) .foregroundColor(Color.gray22)
.frame(height: keyboardHandler.keyboardHeight) .frame(height: keyboardHandler.keyboardHeight)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
if proxy.safeAreaInsets.bottom > 0 { if proxy.safeAreaInsets.bottom > 0 {
Rectangle() Rectangle()
.foregroundColor(Color(hex: "222222")) .foregroundColor(Color.gray22)
.frame(height: 15.3) .frame(height: 15.3)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
@ -221,6 +315,16 @@ struct CreatorCommunityWriteView: View {
sourceType: .photoLibrary sourceType: .photoLibrary
) )
} }
if isShowRecordingVoiceView {
CreatorCommunityRecordingVoiceView(
isShowing: $isShowRecordingVoiceView,
isShowPopup: $viewModel.isShowPopup,
errorMessage: $viewModel.errorMessage,
fileName: $fileName,
soundData: $viewModel.soundData
)
}
} }
.onTapGesture { hideKeyboard() } .onTapGesture { hideKeyboard() }
.edgesIgnoringSafeArea(.bottom) .edgesIgnoringSafeArea(.bottom)
@ -232,7 +336,7 @@ struct CreatorCommunityWriteView: View {
.padding(.vertical, 13.3) .padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center) .frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12)) .font(.custom(Font.medium.rawValue, size: 12))
.background(Color(hex: "9970ff")) .background(Color.button)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.cornerRadius(20) .cornerRadius(20)
@ -244,6 +348,21 @@ struct CreatorCommunityWriteView: View {
} }
} }
} }
private func deleteAudioFile() {
do {
try FileManager.default.removeItem(at: getAudioFileURL())
} catch {}
}
private func getAudioFileURL() -> URL {
return getDocumentsDirectory().appendingPathComponent(fileName)
}
private func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
} }
struct CreatorCommunityWriteView_Previews: PreviewProvider { struct CreatorCommunityWriteView_Previews: PreviewProvider {

View File

@ -20,16 +20,39 @@ final class CreatorCommunityWriteViewModel: ObservableObject {
@Published var content = "" @Published var content = ""
@Published var isAdult = false @Published var isAdult = false
@Published var isPriceFree = true {
didSet {
if isPriceFree {
priceString = "0"
}
}
}
@Published var isAvailableComment = true @Published var isAvailableComment = true
@Published var postImage: UIImage? = nil @Published var postImage: UIImage? = nil
@Published var priceString = "0" {
didSet {
if priceString.count > 5 {
priceString = String(priceString.prefix(5))
} else {
if let price = Int(priceString) {
self.price = price
} else {
self.price = 0
}
}
}
}
@Published var price = 0
@Published var soundData: Data? = nil
var placeholder = "내용을 입력하세요" var placeholder = "내용을 입력하세요"
func createCommunityPost(onSuccess: @escaping () -> Void) { func createCommunityPost(onSuccess: @escaping () -> Void) {
if !isLoading && validateData() { if !isLoading && validateData() {
isLoading = true isLoading = true
let request = CreateCommunityPostRequest(content: content, isAdult: isAdult, isCommentAvailable: isAvailableComment) let request = CreateCommunityPostRequest(content: content, price: price, isAdult: isAdult, isCommentAvailable: isAvailableComment)
var multipartData = [MultipartFormData]() var multipartData = [MultipartFormData]()
let encoder = JSONEncoder() let encoder = JSONEncoder()
@ -43,7 +66,19 @@ final class CreatorCommunityWriteViewModel: ObservableObject {
provider: .data(imageData), provider: .data(imageData),
name: "postImage", name: "postImage",
fileName: "\(UUID().uuidString)_\(Date().timeIntervalSince1970 * 1000).jpg", fileName: "\(UUID().uuidString)_\(Date().timeIntervalSince1970 * 1000).jpg",
mimeType: "image/*") mimeType: "image/*"
)
)
}
if let soundData = soundData {
multipartData.append(
MultipartFormData(
provider: .data(soundData),
name: "audioFile",
fileName: "\(UUID().uuidString)_\(Date().timeIntervalSince1970 * 1000).m4a",
mimeType: "audio/m4a"
)
) )
} }
@ -103,6 +138,12 @@ final class CreatorCommunityWriteViewModel: ObservableObject {
return false return false
} }
if !isPriceFree && price < 5 {
errorMessage = "최소금액은 5캔 입니다."
isShowPopup = true
return false
}
return true return true
} }
} }

View File

@ -16,6 +16,9 @@ struct UserProfileFanTalkAllView: View {
@State private var cheersContent: String = "" @State private var cheersContent: String = ""
@State private var cheersId: Int = 0 @State private var cheersId: Int = 0
@State private var memberId: Int = 0
@State private var isShowMemberProfilePopup: Bool = false
var body: some View { var body: some View {
GeometryReader { proxy in GeometryReader { proxy in
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
@ -26,17 +29,17 @@ struct UserProfileFanTalkAllView: View {
HStack(spacing: 6.7) { HStack(spacing: 6.7) {
Text("응원") Text("응원")
.font(.custom(Font.medium.rawValue, size: 14.7)) .font(.custom(Font.medium.rawValue, size: 14.7))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color.grayee)
Text("\(viewModel.cheersTotalCount)") Text("\(viewModel.cheersTotalCount)")
.font(.custom(Font.medium.rawValue, size: 12)) .font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color.gray77)
} }
.padding(.top, 20) .padding(.top, 20)
Rectangle() Rectangle()
.frame(height: 1) .frame(height: 1)
.foregroundColor(Color(hex: "909090").opacity(0.5)) .foregroundColor(Color.gray90.opacity(0.5))
.padding(.top, 13.3) .padding(.top, 13.3)
HStack(spacing: 0) { HStack(spacing: 0) {
@ -44,8 +47,8 @@ struct UserProfileFanTalkAllView: View {
.autocapitalization(.none) .autocapitalization(.none)
.disableAutocorrection(true) .disableAutocorrection(true)
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color.grayee)
.accentColor(Color(hex: "3bb9f1")) .accentColor(Color.button)
.keyboardType(.default) .keyboardType(.default)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
@ -61,18 +64,18 @@ struct UserProfileFanTalkAllView: View {
cheersContent = "" cheersContent = ""
} }
} }
.background(Color(hex: "232323")) .background(Color.gray23)
.cornerRadius(10) .cornerRadius(10)
.overlay( .overlay(
RoundedRectangle(cornerRadius: 10) RoundedRectangle(cornerRadius: 10)
.strokeBorder(lineWidth: 1) .strokeBorder(lineWidth: 1)
.foregroundColor(Color(hex: "3bb9f1")) .foregroundColor(Color.button)
) )
.padding(.top, 13.3) .padding(.top, 13.3)
Rectangle() Rectangle()
.frame(height: 1) .frame(height: 1)
.foregroundColor(Color(hex: "909090").opacity(0.5)) .foregroundColor(Color.gray90.opacity(0.5))
.padding(.top, 13.3) .padding(.top, 13.3)
ScrollView(.vertical, showsIndicators: false) { ScrollView(.vertical, showsIndicators: false) {
@ -96,6 +99,10 @@ struct UserProfileFanTalkAllView: View {
onClickDelete: { cheersId in onClickDelete: { cheersId in
self.cheersId = cheersId self.cheersId = cheersId
viewModel.isShowCheersDeleteView = true viewModel.isShowCheersDeleteView = true
},
onClickProfile: {
self.memberId = $0
self.isShowMemberProfilePopup = true
} }
) )
.onAppear { .onAppear {
@ -110,7 +117,7 @@ struct UserProfileFanTalkAllView: View {
} else { } else {
Text("응원이 없습니다.\n\n처음으로 응원을 해보세요!") Text("응원이 없습니다.\n\n처음으로 응원을 해보세요!")
.font(.custom(Font.light.rawValue, size: 13.3)) .font(.custom(Font.light.rawValue, size: 13.3))
.foregroundColor(Color(hex: "bbbbbb")) .foregroundColor(Color.graybb)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.padding(.vertical, 60) .padding(.vertical, 60)
@ -136,7 +143,7 @@ struct UserProfileFanTalkAllView: View {
.padding(.vertical, 13.3) .padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center) .frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12)) .font(.custom(Font.medium.rawValue, size: 12))
.background(Color(hex: "9970ff")) .background(Color.button)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
.cornerRadius(20) .cornerRadius(20)
@ -171,6 +178,10 @@ struct UserProfileFanTalkAllView: View {
) )
} }
} }
if isShowMemberProfilePopup {
MemberProfileDialog(isShowing: $isShowMemberProfilePopup, memberId: memberId)
}
} }
} }
} }

View File

@ -16,6 +16,7 @@ struct UserProfileFanTalkCheersItemView: View {
let modifyCheer: (Int, String) -> Void let modifyCheer: (Int, String) -> Void
let reportPopup: (Int) -> Void let reportPopup: (Int) -> Void
let onClickDelete: (Int) -> Void let onClickDelete: (Int) -> Void
let onClickProfile: (Int) -> Void
@State var replyContent: String = "" @State var replyContent: String = ""
@State var isShowInputReply = false @State var isShowInputReply = false
@ -34,6 +35,11 @@ struct UserProfileFanTalkCheersItemView: View {
.resizable() .resizable()
.frame(width: 33.3, height: 33.3) .frame(width: 33.3, height: 33.3)
.clipShape(Circle()) .clipShape(Circle())
.onTapGesture {
if UserDefaults.int(forKey: .userId) != cheersItem.memberId {
onClickProfile(cheersItem.memberId)
}
}
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
Text("\(cheersItem.nickname)") Text("\(cheersItem.nickname)")
@ -51,10 +57,10 @@ struct UserProfileFanTalkCheersItemView: View {
.autocapitalization(.none) .autocapitalization(.none)
.disableAutocorrection(true) .disableAutocorrection(true)
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color.grayee)
.padding(13.3) .padding(13.3)
.background(Color(hex: "232323")) .background(Color.gray23)
.accentColor(Color(hex: "3bb9f1")) .accentColor(Color.button)
.keyboardType(.default) .keyboardType(.default)
.cornerRadius(10) .cornerRadius(10)
.overlay( .overlay(
@ -65,7 +71,7 @@ struct UserProfileFanTalkCheersItemView: View {
Text("수정") Text("수정")
.font(.custom(Font.bold.rawValue, size: 13.3)) .font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color(hex: "ffffff")) .foregroundColor(Color.white)
.padding(13.3) .padding(13.3)
.background(Color.button) .background(Color.button)
.cornerRadius(6.7) .cornerRadius(6.7)
@ -78,7 +84,7 @@ struct UserProfileFanTalkCheersItemView: View {
.font(.custom(Font.bold.rawValue, size: 13.3)) .font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color.button) .foregroundColor(Color.button)
.padding(13.3) .padding(13.3)
.background(Color(hex: "222222")) .background(Color.gray22)
.cornerRadius(6.7) .cornerRadius(6.7)
.onTapGesture { .onTapGesture {
isModeModify = false isModeModify = false
@ -100,23 +106,23 @@ struct UserProfileFanTalkCheersItemView: View {
.autocapitalization(.none) .autocapitalization(.none)
.disableAutocorrection(true) .disableAutocorrection(true)
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color.grayee)
.padding(13.3) .padding(13.3)
.background(Color(hex: "232323")) .background(Color.gray23)
.accentColor(Color(hex: "3bb9f1")) .accentColor(Color.button)
.keyboardType(.default) .keyboardType(.default)
.cornerRadius(10) .cornerRadius(10)
.overlay( .overlay(
RoundedRectangle(cornerRadius: 10) RoundedRectangle(cornerRadius: 10)
.strokeBorder(lineWidth: 1) .strokeBorder(lineWidth: 1)
.foregroundColor(Color(hex: "3bb9f1")) .foregroundColor(Color.button)
) )
Text("등록") Text("등록")
.font(.custom(Font.bold.rawValue, size: 13.3)) .font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color(hex: "ffffff")) .foregroundColor(Color.white)
.padding(13.3) .padding(13.3)
.background(Color(hex: "3bb9f1")) .background(Color.button)
.cornerRadius(6.7) .cornerRadius(6.7)
.onTapGesture { .onTapGesture {
if cheersItem.replyList.count > 0 { if cheersItem.replyList.count > 0 {
@ -143,23 +149,24 @@ struct UserProfileFanTalkCheersItemView: View {
VStack(alignment: .leading, spacing: 8.3) { VStack(alignment: .leading, spacing: 8.3) {
Text(reply.content) Text(reply.content)
.font(.custom(Font.medium.rawValue, size: 12)) .font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "ffffff")) .foregroundColor(Color.white)
.frame(minWidth: 100) .frame(minWidth: 100)
.padding(.horizontal, 6.7) .padding(.horizontal, 6.7)
.padding(.vertical, 6.7) .padding(.vertical, 6.7)
.background(Color.button.opacity(0.3)) .background(Color.button.opacity(0.3))
.cornerRadius(16.7) .cornerRadius(16.7)
.padding(.top, 18.3) .padding(.top, 18.3)
.fixedSize(horizontal: false, vertical: true)
HStack(spacing: 6.7) { HStack(spacing: 6.7) {
Text(reply.date) Text(reply.date)
.font(.custom(Font.medium.rawValue, size: 10.7)) .font(.custom(Font.medium.rawValue, size: 10.7))
.foregroundColor(Color(hex: "525252")) .foregroundColor(Color.gray52)
if userId == UserDefaults.int(forKey: .userId) { if userId == UserDefaults.int(forKey: .userId) {
Text("답글 수정") Text("답글 수정")
.font(.custom(Font.medium.rawValue, size: 10.7)) .font(.custom(Font.medium.rawValue, size: 10.7))
.foregroundColor(Color(hex: "9970ff")) .foregroundColor(Color.button)
.onTapGesture { .onTapGesture {
self.replyContent = reply.content self.replyContent = reply.content
isShowInputReply = true isShowInputReply = true
@ -181,7 +188,7 @@ struct UserProfileFanTalkCheersItemView: View {
Rectangle() Rectangle()
.frame(height: 1) .frame(height: 1)
.foregroundColor(Color(hex: "909090").opacity(0.5)) .foregroundColor(Color.gray90.opacity(0.5))
.padding(.top, 13.3) .padding(.top, 13.3)
} }
.frame(width: screenSize().width - 26.7) .frame(width: screenSize().width - 26.7)
@ -191,7 +198,7 @@ struct UserProfileFanTalkCheersItemView: View {
if cheersItem.memberId != UserDefaults.int(forKey: .userId) { if cheersItem.memberId != UserDefaults.int(forKey: .userId) {
Text("신고하기") Text("신고하기")
.font(.custom(Font.medium.rawValue, size: 14)) .font(.custom(Font.medium.rawValue, size: 14))
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color.gray77)
.onTapGesture { .onTapGesture {
reportPopup(cheersItem.cheersId) reportPopup(cheersItem.cheersId)
isShowPopupMenu = false isShowPopupMenu = false
@ -201,7 +208,7 @@ struct UserProfileFanTalkCheersItemView: View {
if cheersItem.memberId == UserDefaults.int(forKey: .userId) { if cheersItem.memberId == UserDefaults.int(forKey: .userId) {
Text("수정") Text("수정")
.font(.custom(Font.medium.rawValue, size: 14)) .font(.custom(Font.medium.rawValue, size: 14))
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color.gray77)
.onTapGesture { .onTapGesture {
isModeModify = true isModeModify = true
isShowPopupMenu = false isShowPopupMenu = false
@ -214,7 +221,7 @@ struct UserProfileFanTalkCheersItemView: View {
{ {
Text("삭제") Text("삭제")
.font(.custom(Font.medium.rawValue, size: 14)) .font(.custom(Font.medium.rawValue, size: 14))
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color.gray77)
.onTapGesture { .onTapGesture {
onClickDelete(cheersItem.cheersId) onClickDelete(cheersItem.cheersId)
isShowPopupMenu = false isShowPopupMenu = false
@ -222,7 +229,7 @@ struct UserProfileFanTalkCheersItemView: View {
} }
} }
.padding(10) .padding(10)
.background(Color(hex: "222222")) .background(Color.gray22)
} }
} }
.contentShape(Rectangle()) .contentShape(Rectangle())

View File

@ -17,6 +17,7 @@ struct UserProfileFanTalkView: View {
let errorPopup: (String) -> Void let errorPopup: (String) -> Void
let reportPopup: (Int) -> Void let reportPopup: (Int) -> Void
let deletePopup: (Int) -> Void let deletePopup: (Int) -> Void
let profilePopup: (Int) -> Void
@Binding var isLoading: Bool @Binding var isLoading: Bool
@State private var cheersContent: String = "" @State private var cheersContent: String = ""
@ -26,13 +27,13 @@ struct UserProfileFanTalkView: View {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("팬 Talk") Text("팬 Talk")
.font(.custom(Font.bold.rawValue, size: 16.7)) .font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color.grayee)
Spacer() Spacer()
Text("전체보기") Text("전체보기")
.font(.custom(Font.light.rawValue, size: 11.3)) .font(.custom(Font.light.rawValue, size: 11.3))
.foregroundColor(Color(hex: "bbbbbb")) .foregroundColor(Color.graybb)
.onTapGesture { .onTapGesture {
AppState.shared.setAppStep(step: .userProfileFanTalkAll(userId: userId)) AppState.shared.setAppStep(step: .userProfileFanTalkAll(userId: userId))
} }
@ -43,17 +44,17 @@ struct UserProfileFanTalkView: View {
HStack(spacing: 6.7) { HStack(spacing: 6.7) {
Text("응원") Text("응원")
.font(.custom(Font.medium.rawValue, size: 14.7)) .font(.custom(Font.medium.rawValue, size: 14.7))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color.grayee)
Text("\(cheers.totalCount)") Text("\(cheers.totalCount)")
.font(.custom(Font.medium.rawValue, size: 12)) .font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color.gray77)
} }
.padding(.top, 20) .padding(.top, 20)
Rectangle() Rectangle()
.frame(height: 1) .frame(height: 1)
.foregroundColor(Color(hex: "909090").opacity(0.5)) .foregroundColor(Color.gray90.opacity(0.5))
.padding(.top, 13.3) .padding(.top, 13.3)
HStack(spacing: 0) { HStack(spacing: 0) {
@ -61,8 +62,8 @@ struct UserProfileFanTalkView: View {
.autocapitalization(.none) .autocapitalization(.none)
.disableAutocorrection(true) .disableAutocorrection(true)
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color.grayee)
.accentColor(Color(hex: "3bb9f1")) .accentColor(Color.button)
.keyboardType(.default) .keyboardType(.default)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
@ -78,18 +79,18 @@ struct UserProfileFanTalkView: View {
cheersContent = "" cheersContent = ""
} }
} }
.background(Color(hex: "232323")) .background(Color.gray23)
.cornerRadius(10) .cornerRadius(10)
.overlay( .overlay(
RoundedRectangle(cornerRadius: 10) RoundedRectangle(cornerRadius: 10)
.strokeBorder(lineWidth: 1) .strokeBorder(lineWidth: 1)
.foregroundColor(Color(hex: "9970ff")) .foregroundColor(Color.button)
) )
.padding(.top, 13.3) .padding(.top, 13.3)
Rectangle() Rectangle()
.frame(height: 1) .frame(height: 1)
.foregroundColor(Color(hex: "909090").opacity(0.5)) .foregroundColor(Color.gray90.opacity(0.5))
.padding(.top, 13.3) .padding(.top, 13.3)
VStack(spacing: 20) { VStack(spacing: 20) {
@ -110,6 +111,9 @@ struct UserProfileFanTalkView: View {
}, },
onClickDelete: { cheersId in onClickDelete: { cheersId in
deletePopup(cheersId) deletePopup(cheersId)
},
onClickProfile: {
profilePopup($0)
} }
) )
.onTapGesture { .onTapGesture {
@ -119,7 +123,7 @@ struct UserProfileFanTalkView: View {
} else { } else {
Text("응원이 없습니다.\n\n처음으로 응원을 해보세요!") Text("응원이 없습니다.\n\n처음으로 응원을 해보세요!")
.font(.custom(Font.light.rawValue, size: 13.3)) .font(.custom(Font.light.rawValue, size: 13.3))
.foregroundColor(Color(hex: "bbbbbb")) .foregroundColor(Color.graybb)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.padding(.vertical, 60) .padding(.vertical, 60)

View File

@ -81,6 +81,9 @@ struct GetAudioContentListItem: Decodable {
let isPin: Bool let isPin: Bool
let isAdult: Bool let isAdult: Bool
let isScheduledToOpen: Bool let isScheduledToOpen: Bool
let isRented: Bool
let isOwned: Bool
let isSoldOut: Bool
} }
struct GetCreatorActivitySummary: Decodable { struct GetCreatorActivitySummary: Decodable {

View File

@ -76,7 +76,10 @@ struct UserProfileContentView_Previews: PreviewProvider {
commentCount: 0, commentCount: 0,
isPin: true, isPin: true,
isAdult: false, isAdult: false,
isScheduledToOpen: false isScheduledToOpen: false,
isRented: false,
isOwned: false,
isSoldOut: false
) )
] ]
) )

View File

@ -12,6 +12,9 @@ struct UserProfileView: View {
let userId: Int let userId: Int
@StateObject var viewModel = UserProfileViewModel() @StateObject var viewModel = UserProfileViewModel()
@State private var memberId: Int = 0
@State private var isShowMemberProfilePopup: Bool = false
var body: some View { var body: some View {
GeometryReader { proxy in GeometryReader { proxy in
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
@ -196,6 +199,10 @@ struct UserProfileView: View {
viewModel.cheersId = cheerId viewModel.cheersId = cheerId
viewModel.isShowCheersDeleteView = true viewModel.isShowCheersDeleteView = true
}, },
profilePopup: {
self.memberId = $0
self.isShowMemberProfilePopup = true
},
isLoading: $viewModel.isLoading isLoading: $viewModel.isLoading
) )
.padding(.top, 26.7) .padding(.top, 26.7)
@ -311,6 +318,10 @@ struct UserProfileView: View {
} }
) )
} }
if isShowMemberProfilePopup {
MemberProfileDialog(isShowing: $isShowMemberProfilePopup, memberId: memberId)
}
} }
} }
.sheet( .sheet(

View File

@ -14,36 +14,37 @@ struct FollowCreatorView: View {
var body: some View { var body: some View {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) { VStack(spacing: 0) {
DetailNavigationBar(title: "팔로잉 채널리스트") DetailNavigationBar(title: "팔로잉 리스트")
HStack(spacing: 0) { HStack(spacing: 0) {
Text("") Text("")
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color.grayee)
Text("\(viewModel.totalCount)") Text("\(viewModel.totalCount)")
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "dd4500")) .foregroundColor(Color.mainRed3)
Text("") Text("")
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color.grayee)
Spacer() Spacer()
} }
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
.padding(.top, 6.7) .padding(.top, 6.7)
ScrollView(.vertical, showsIndicators: false) { if viewModel.totalCount > 0 {
VStack(spacing: 13.3) { ScrollView(.vertical, showsIndicators: false) {
ForEach(0..<viewModel.creatorList.count, id: \.self) { index in VStack(spacing: 13.3) {
let creator = viewModel.creatorList[index] ForEach(0..<viewModel.creatorList.count, id: \.self) { index in
let creator = viewModel.creatorList[index]
FollowCreatorItemView(
creator: creator, FollowCreatorItemView(
onClickFollow: { viewModel.creatorFollow(userId: $0) }, creator: creator,
onClickUnFollow: { viewModel.creatorUnFollow(userId: $0) } onClickFollow: { viewModel.creatorFollow(userId: $0) },
) onClickUnFollow: { viewModel.creatorUnFollow(userId: $0) }
)
.padding(.horizontal, 20) .padding(.horizontal, 20)
.onTapGesture { .onTapGesture {
AppState.shared AppState.shared
@ -54,9 +55,15 @@ struct FollowCreatorView: View {
viewModel.getFollowedCreatorAllList() viewModel.getFollowedCreatorAllList()
} }
} }
}
} }
.padding(.top, 13.3)
} }
.padding(.top, 13.3) } else {
Text("팔로우 중인 채널이 없습니다.")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color.grayee)
.frame(maxHeight: .infinity)
} }
} }
.onAppear { .onAppear {
@ -69,7 +76,7 @@ struct FollowCreatorView: View {
.padding(.vertical, 13.3) .padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center) .frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12)) .font(.custom(Font.medium.rawValue, size: 12))
.background(Color(hex: "3bb9f1")) .background(Color.button)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
.cornerRadius(20) .cornerRadius(20)

View File

@ -73,8 +73,8 @@ final class LiveRepository {
return api.requestPublisher(.getRoomInfo(roomId: roomId)) return api.requestPublisher(.getRoomInfo(roomId: roomId))
} }
func donation(roomId: Int, can: Int, message: String = "") -> AnyPublisher<Response, MoyaError> { func donation(roomId: Int, can: Int, message: String = "", isSecret: Bool = false) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.donation(request: LiveRoomDonationRequest(roomId: roomId, can: can, message: message))) return api.requestPublisher(.donation(request: LiveRoomDonationRequest(roomId: roomId, can: can, message: message, isSecret: isSecret)))
} }
func refundDonation(roomId: Int) -> AnyPublisher<Response, MoyaError> { func refundDonation(roomId: Int) -> AnyPublisher<Response, MoyaError> {

View File

@ -9,7 +9,7 @@ import Foundation
struct LiveRoomChatRawMessage: Codable { struct LiveRoomChatRawMessage: Codable {
enum LiveRoomChatRawMessageType: String, Codable { enum LiveRoomChatRawMessageType: String, Codable {
case DONATION, EDIT_ROOM_INFO, SET_MANAGER, TOGGLE_ROULETTE, ROULETTE_DONATION case DONATION, SECRET_DONATION, EDIT_ROOM_INFO, SET_MANAGER, TOGGLE_ROULETTE, ROULETTE_DONATION
} }
let type: LiveRoomChatRawMessageType let type: LiveRoomChatRawMessageType

View File

@ -43,7 +43,7 @@ struct LiveRoomDonationChatItemView: View {
.font(.system(size: 15)) .font(.system(size: 15))
.foregroundColor(Color(hex: "fdca2f")) .foregroundColor(Color(hex: "fdca2f"))
Text("을 후원하셨습니다.") Text(chatMessage.chat.contains("비밀") ? "을 비밀후원하셨습니다.💰🪙" : "을 후원하셨습니다.💰🪙")
.font(.system(size: 15)) .font(.system(size: 15))
.foregroundColor(.white) .foregroundColor(.white)
} }
@ -58,6 +58,7 @@ struct LiveRoomDonationChatItemView: View {
.padding(13) .padding(13)
.frame(width: screenSize().width - 86, alignment: .leading) .frame(width: screenSize().width - 86, alignment: .leading)
.background( .background(
chatMessage.chat.contains("비밀") ? Color(hex: "333333").opacity(0.8) :
chatMessage.can >= 10000 ? Color(hex: "c25264").opacity(0.8) : chatMessage.can >= 10000 ? Color(hex: "c25264").opacity(0.8) :
chatMessage.can >= 5000 ? Color(hex: "d85e37").opacity(0.8) : chatMessage.can >= 5000 ? Color(hex: "d85e37").opacity(0.8) :
chatMessage.can >= 1000 ? Color(hex: "d38c38").opacity(0.8) : chatMessage.can >= 1000 ? Color(hex: "d38c38").opacity(0.8) :

View File

@ -9,8 +9,10 @@ import SwiftUI
struct LiveRoomDonationMessageDialog: View { struct LiveRoomDonationMessageDialog: View {
@StateObject var viewModel: LiveRoomViewModel
@Binding var isShowing: Bool @Binding var isShowing: Bool
@StateObject var viewModel = LiveRoomViewModel()
@State var creatorId = 0
var body: some View { var body: some View {
ZStack { ZStack {
@ -45,7 +47,7 @@ struct LiveRoomDonationMessageDialog: View {
ForEach(0..<viewModel.donationMessageList.count, id: \.self) { index in ForEach(0..<viewModel.donationMessageList.count, id: \.self) { index in
let donationMessage = viewModel.donationMessageList[index] let donationMessage = viewModel.donationMessageList[index]
LiveRoomDonationMessageItemView(message: donationMessage) { LiveRoomDonationMessageItemView(message: donationMessage, creatorId: creatorId) {
viewModel.deleteDonationMessage(uuid: $0) viewModel.deleteDonationMessage(uuid: $0)
} }
.onTapGesture { .onTapGesture {
@ -79,7 +81,7 @@ struct LiveRoomDonationMessageDialog: View {
.padding(.vertical, 13.3) .padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center) .frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12)) .font(.custom(Font.medium.rawValue, size: 12))
.background(Color(hex: "9970ff")) .background(Color.button)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
.cornerRadius(20) .cornerRadius(20)
@ -89,6 +91,7 @@ struct LiveRoomDonationMessageDialog: View {
} }
.onAppear { .onAppear {
viewModel.getDonationMessageList() viewModel.getDonationMessageList()
creatorId = viewModel.liveRoomInfo?.creatorId ?? 0
} }
} }
} }

View File

@ -10,6 +10,7 @@ import SwiftUI
struct LiveRoomDonationMessageItemView: View { struct LiveRoomDonationMessageItemView: View {
let message: LiveRoomDonationMessage let message: LiveRoomDonationMessage
let creatorId: Int
let deleteDonationMessage: (String) -> Void let deleteDonationMessage: (String) -> Void
var body: some View { var body: some View {
@ -32,10 +33,12 @@ struct LiveRoomDonationMessageItemView: View {
Spacer() Spacer()
Image("ic_close_white") if creatorId == UserDefaults.int(forKey: .userId) {
.resizable() Image("ic_close_white")
.frame(width: 13.3, height: 13.3) .resizable()
.onTapGesture { deleteDonationMessage(message.uuid) } .frame(width: 13.3, height: 13.3)
.onTapGesture { deleteDonationMessage(message.uuid) }
}
} }
.padding(13.3) .padding(13.3)
.background(message.canMessage.trimmingCharacters(in: .whitespaces).isEmpty ? Color(hex: "c25264").opacity(0.8) : Color.gray33) .background(message.canMessage.trimmingCharacters(in: .whitespaces).isEmpty ? Color(hex: "c25264").opacity(0.8) : Color.gray33)
@ -51,6 +54,7 @@ struct LiveRoomDonationMessageItemView: View {
canMessage: "10캔을 후원하셨습니다", canMessage: "10캔을 후원하셨습니다",
donationMessage: "ㅅㅅㅅ" donationMessage: "ㅅㅅㅅ"
), ),
creatorId: 0,
deleteDonationMessage: { _ in } deleteDonationMessage: { _ in }
) )
} }
@ -63,6 +67,7 @@ struct LiveRoomDonationMessageItemView: View {
canMessage: "", canMessage: "",
donationMessage: "[테스트] 당첨!" donationMessage: "[테스트] 당첨!"
), ),
creatorId: 0,
deleteDonationMessage: { _ in } deleteDonationMessage: { _ in }
) )
} }

View File

@ -15,6 +15,7 @@ struct LiveRoomInfoEditDialog: View {
@State private var title = "" @State private var title = ""
@State private var notice = "" @State private var notice = ""
@State private var isAdult = false
let placeholder = "라이브 공지를 입력하세요" let placeholder = "라이브 공지를 입력하세요"
@ -23,24 +24,26 @@ struct LiveRoomInfoEditDialog: View {
let isLoading: Bool let isLoading: Bool
let coverImageUrl: String? let coverImageUrl: String?
let coverImage: UIImage? let coverImage: UIImage?
var confirmAction: (String, String) -> Void var confirmAction: (String, String, Bool) -> Void
init( init(
isShowing: Binding<Bool>, isShowing: Binding<Bool>,
isShowPhotoPicker: Binding<Bool>, isShowPhotoPicker: Binding<Bool>,
viewModel: LiveRoomViewModel, viewModel: LiveRoomViewModel,
isAdult: Bool,
isLoading: Bool, isLoading: Bool,
currentTitle: String, currentTitle: String,
currentNotice: String, currentNotice: String,
coverImageUrl: String, coverImageUrl: String,
coverImage: UIImage?, coverImage: UIImage?,
confirmAction: @escaping (String, String) -> Void confirmAction: @escaping (String, String, Bool) -> Void
) { ) {
self._isShowing = isShowing self._isShowing = isShowing
self._isShowPhotoPicker = isShowPhotoPicker self._isShowPhotoPicker = isShowPhotoPicker
self._viewModel = StateObject(wrappedValue: viewModel) self._viewModel = StateObject(wrappedValue: viewModel)
self.isLoading = isLoading self.isLoading = isLoading
self.isAdult = isAdult
self.title = currentTitle self.title = currentTitle
self.notice = currentNotice self.notice = currentNotice
@ -116,6 +119,30 @@ struct LiveRoomInfoEditDialog: View {
ContentInputView() ContentInputView()
.padding(.top, 33.3) .padding(.top, 33.3)
if UserDefaults.bool(forKey: .auth) {
VStack(alignment: .leading, spacing: 8) {
Text("연령제한")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color.grayee)
HStack(spacing: 0) {
Text("19세 이상")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color.grayee)
Spacer()
Image(isAdult ? "btn_toggle_on_big" : "btn_toggle_off_big")
.resizable()
.frame(width: 33.3, height: 20)
.onTapGesture {
isAdult.toggle()
}
}
}
.padding(.top, 33.3)
}
LiveRoomMenuSelectView( LiveRoomMenuSelectView(
menu: $viewModel.menu, menu: $viewModel.menu,
isActivate: $viewModel.isActivateMenu, isActivate: $viewModel.isActivateMenu,
@ -152,7 +179,8 @@ struct LiveRoomInfoEditDialog: View {
.onTapGesture { .onTapGesture {
confirmAction( confirmAction(
title, title,
notice.trimmingCharacters(in: .whitespacesAndNewlines) != placeholder ? notice : "" notice.trimmingCharacters(in: .whitespacesAndNewlines) != placeholder ? notice : "",
isAdult
) )
isShowing = false isShowing = false
} }
@ -169,7 +197,7 @@ struct LiveRoomInfoEditDialog: View {
} }
} }
} }
.background(Color(hex: "222222").edgesIgnoringSafeArea(.all)) .background(Color.gray22.edgesIgnoringSafeArea(.all))
if viewModel.isShowPopup { if viewModel.isShowPopup {
LiveRoomDialogView( LiveRoomDialogView(
@ -198,7 +226,7 @@ struct LiveRoomInfoEditDialog: View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
Text("제목") Text("제목")
.font(.custom(Font.bold.rawValue, size: 16.7)) .font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color.grayee)
TextField("라이브 제목을 입력하세요", text: $title) TextField("라이브 제목을 입력하세요", text: $title)
.autocapitalization(.none) .autocapitalization(.none)

View File

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

View File

@ -16,4 +16,5 @@ struct EditLiveRoomInfoRequest: Encodable {
var menuPanId: Int = 0 var menuPanId: Int = 0
var menuPan: String = "" var menuPan: String = ""
var isActiveMenuPan: Bool? = nil var isActiveMenuPan: Bool? = nil
var isAdult: Bool? = nil
} }

View File

@ -11,5 +11,6 @@ struct LiveRoomDonationRequest: Encodable {
let roomId: Int let roomId: Int
let can: Int let can: Int
let message: String let message: String
var isSecret: Bool = false
let container: String = "ios" let container: String = "ios"
} }

View File

@ -43,6 +43,13 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
@Published var isShowReportPopup = false @Published var isShowReportPopup = false
@Published var isShowErrorPopup = false @Published var isShowErrorPopup = false
@Published var isShowUserProfilePopup = false @Published var isShowUserProfilePopup = false
@Published var changeIsAdult = false {
didSet {
if changeIsAdult && !UserDefaults.bool(forKey: .auth) {
agora.speakerMute(true)
}
}
}
@Published var popupContent = "" @Published var popupContent = ""
@Published var popupCancelTitle: String? = nil @Published var popupCancelTitle: String? = nil
@ -189,6 +196,34 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
var timer: DispatchSourceTimer? var timer: DispatchSourceTimer?
private var blockedMemberIdList = Set<Int>()
func getBlockedMemberIdList() {
userRepository.getBlockedMemberIdList()
.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<[Int]>.self, from: responseData)
if let data = decoded.data, decoded.success {
self.blockedMemberIdList.removeAll()
self.blockedMemberIdList.formUnion(data)
}
} catch {
}
}
.store(in: &subscription)
}
func setOriginOffset(_ offset: CGFloat) { func setOriginOffset(_ offset: CGFloat) {
guard !isCheckedOriginOffset else { return } guard !isCheckedOriginOffset else { return }
self.originOffset = offset self.originOffset = offset
@ -314,6 +349,10 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
getTotalDonationCan() getTotalDonationCan()
if data.isAdult && !UserDefaults.bool(forKey: .auth) {
changeIsAdult = true
}
if (userId > 0 && data.creatorId == UserDefaults.int(forKey: .userId)) { if (userId > 0 && data.creatorId == UserDefaults.int(forKey: .userId)) {
let nickname = getUserNicknameAndProfileUrl(accountId: userId).nickname let nickname = getUserNicknameAndProfileUrl(accountId: userId).nickname
onSuccess(nickname) onSuccess(nickname)
@ -380,11 +419,11 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
} }
} }
func donation(can: Int, message: String = "") { func donation(can: Int, message: String = "", isSecret: Bool = false) {
if can > 0 { if can > 0 {
isLoading = true isLoading = true
repository.donation(roomId: AppState.shared.roomId, can: can, message: message) repository.donation(roomId: AppState.shared.roomId, can: can, message: message, isSecret: isSecret)
.sink { result in .sink { result in
switch result { switch result {
case .finished: case .finished:
@ -402,9 +441,16 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
self.isLoading = false self.isLoading = false
if decoded.success { if decoded.success {
let rawMessage = "\(can)캔을 후원하셨습니다." var rawMessage = ""
if isSecret {
rawMessage = "\(can)캔을 비밀후원하셨습니다.💰🪙"
} else {
rawMessage = "\(can)캔을 후원하셨습니다.💰🪙"
}
let donationRawMessage = LiveRoomChatRawMessage( let donationRawMessage = LiveRoomChatRawMessage(
type: .DONATION, type: isSecret ? .SECRET_DONATION : .DONATION,
message: rawMessage, message: rawMessage,
can: can, can: can,
signature: decoded.data, signature: decoded.data,
@ -414,36 +460,68 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
UserDefaults.set(UserDefaults.int(forKey: .can) - can, forKey: .can) UserDefaults.set(UserDefaults.int(forKey: .can) - can, forKey: .can)
agora.sendRawMessageToGroup( if isSecret {
rawMessage: donationRawMessage, agora.sendRawMessageToPeer(
completion: { [unowned self] errorCode in peerId: String(liveRoomInfo!.creatorId), rawMessage: donationRawMessage,
if errorCode == .errorOk { completion: { [unowned self] errorCode in
let (nickname, profileUrl) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId)) if errorCode == .ok {
self.messages.append( let (nickname, profileUrl) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId))
LiveRoomDonationChat( self.messages.append(
profileUrl: profileUrl, LiveRoomDonationChat(
nickname: nickname, profileUrl: profileUrl,
chat: rawMessage, nickname: nickname,
can: can, chat: rawMessage,
donationMessage: message can: can,
donationMessage: message
)
) )
)
addSignature(signature: decoded.data)
totalDonationCan += can
addSignature(signature: decoded.data) self.messageChangeFlag.toggle()
if self.messages.count > 100 {
self.messageChangeFlag.toggle() self.messages.remove(at: 0)
if self.messages.count > 100 { }
self.messages.remove(at: 0) } else {
refundDonation()
} }
} else { },
fail: { [unowned self] in
refundDonation() refundDonation()
} }
}, )
fail: { [unowned self] in } else {
refundDonation() agora.sendRawMessageToGroup(
} rawMessage: donationRawMessage,
) completion: { [unowned self] errorCode in
if errorCode == .errorOk {
let (nickname, profileUrl) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId))
self.messages.append(
LiveRoomDonationChat(
profileUrl: profileUrl,
nickname: nickname,
chat: rawMessage,
can: can,
donationMessage: message
)
)
totalDonationCan += can
addSignature(signature: decoded.data)
self.messageChangeFlag.toggle()
if self.messages.count > 100 {
self.messages.remove(at: 0)
}
} else {
refundDonation()
}
},
fail: { [unowned self] in
refundDonation()
}
)
}
} else { } else {
if let message = decoded.message { if let message = decoded.message {
self.popupContent = message self.popupContent = message
@ -640,7 +718,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
agora.sendMessageToPeer(peerId: peerId, rawMessage: LiveRoomRequestType.REQUEST_SPEAKER_ALLOW.rawValue.data(using: .utf8)!, completion: nil) agora.sendMessageToPeer(peerId: peerId, rawMessage: LiveRoomRequestType.REQUEST_SPEAKER_ALLOW.rawValue.data(using: .utf8)!, completion: nil)
} }
func editLiveRoomInfo(title: String, notice: String) { func editLiveRoomInfo(title: String, notice: String, isAdult: Bool) {
let request = EditLiveRoomInfoRequest( let request = EditLiveRoomInfoRequest(
title: liveRoomInfo!.title != title ? title : nil, title: liveRoomInfo!.title != title ? title : nil,
notice: liveRoomInfo!.notice != notice ? notice : nil, notice: liveRoomInfo!.notice != notice ? notice : nil,
@ -649,10 +727,11 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
timezone: nil, timezone: nil,
menuPanId: isActivateMenu ? menuId : 0, menuPanId: isActivateMenu ? menuId : 0,
menuPan: isActivateMenu ? menu : "", menuPan: isActivateMenu ? menu : "",
isActiveMenuPan: isActivateMenu isActiveMenuPan: isActivateMenu,
isAdult: liveRoomInfo!.isAdult != isAdult ? isAdult : nil
) )
if (request.title == nil && request.notice == nil && coverImage == nil && menu == liveRoomInfo?.menuPan) { if (request.title == nil && request.notice == nil && coverImage == nil && menu == liveRoomInfo?.menuPan && request.isAdult == nil) {
self.errorMessage = "변경사항이 없습니다." self.errorMessage = "변경사항이 없습니다."
self.isShowErrorPopup = true self.isShowErrorPopup = true
return return
@ -663,7 +742,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
let encoder = JSONEncoder() let encoder = JSONEncoder()
encoder.outputFormatting = .withoutEscapingSlashes encoder.outputFormatting = .withoutEscapingSlashes
if (request.title != nil || request.notice != nil || menu != liveRoomInfo?.menuPan) { if (request.title != nil || request.notice != nil || request.isAdult != nil || menu != liveRoomInfo?.menuPan) {
let jsonData = try? encoder.encode(request) let jsonData = try? encoder.encode(request)
if let jsonData = jsonData { if let jsonData = jsonData {
multipartData.append(MultipartFormData(provider: .data(jsonData), name: "request")) multipartData.append(MultipartFormData(provider: .data(jsonData), name: "request"))
@ -814,22 +893,66 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
.store(in: &subscription) .store(in: &subscription)
} }
func kickOut() { func shareRoom() {
repository.kickOut(roomId: AppState.shared.roomId, userId: kickOutId) guard let link = URL(string: "https://sodalive.net/?room_id=\(AppState.shared.roomId)") else { return }
.sink { result in let dynamicLinksDomainURIPrefix = "https://sodalive.page.link"
switch result { guard let linkBuilder = DynamicLinkComponents(link: link, domainURIPrefix: dynamicLinksDomainURIPrefix) else {
case .finished: self.errorMessage = "공유링크를 생성하지 못했습니다.\n다시 시도해 주세요."
DEBUG_LOG("finish") self.isShowErrorPopup = true
case .failure(let error): return
ERROR_LOG(error.localizedDescription) }
}
} receiveValue: { _ in
}
.store(in: &subscription)
let nickname = getUserNicknameAndProfileUrl(accountId: kickOutId).nickname 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() {
if UserDefaults.int(forKey: .userId) == liveRoomInfo?.creatorId { if UserDefaults.int(forKey: .userId) == liveRoomInfo?.creatorId {
repository.kickOut(roomId: AppState.shared.roomId, userId: kickOutId)
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { _ in
}
.store(in: &subscription)
let nickname = getUserNicknameAndProfileUrl(accountId: kickOutId).nickname
agora.sendMessageToPeer(peerId: String(kickOutId), rawMessage: LiveRoomRequestType.KICK_OUT.rawValue.data(using: .utf8)!, completion: { [unowned self] errorCode in agora.sendMessageToPeer(peerId: String(kickOutId), rawMessage: LiveRoomRequestType.KICK_OUT.rawValue.data(using: .utf8)!, completion: { [unowned self] errorCode in
if errorCode == .ok { if errorCode == .ok {
self.popupContent = "\(nickname)님을 내보냈습니다." self.popupContent = "\(nickname)님을 내보냈습니다."
@ -1248,6 +1371,8 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
} }
func userBlock(onSuccess: @escaping (Int) -> Void) { func userBlock(onSuccess: @escaping (Int) -> Void) {
blockedMemberIdList.insert(reportUserId)
isLoading = true isLoading = true
userRepository.memberBlock(userId: reportUserId) userRepository.memberBlock(userId: reportUserId)
.sink { result in .sink { result in
@ -1291,6 +1416,8 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
} }
func userUnBlock() { func userUnBlock() {
blockedMemberIdList.remove(reportUserId)
isLoading = true isLoading = true
userRepository.memberUnBlock(userId: reportUserId) userRepository.memberUnBlock(userId: reportUserId)
.sink { result in .sink { result in
@ -1738,7 +1865,7 @@ extension LiveRoomViewModel: AgoraRtmDelegate {
self.popupConfirmTitle = "스피커로 초대" self.popupConfirmTitle = "스피커로 초대"
self.popupConfirmAction = { self.popupConfirmAction = {
self.isShowPopup = false self.isShowPopup = false
if self.liveRoomInfo!.speakerList.count <= 4 { if self.liveRoomInfo!.speakerList.count <= 5 {
self.requestSpeakerAllow(peerId) self.requestSpeakerAllow(peerId)
} else { } else {
self.errorMessage = "스피커 정원이 초과되었습니다." self.errorMessage = "스피커 정원이 초과되었습니다."
@ -1814,6 +1941,31 @@ extension LiveRoomViewModel: AgoraRtmDelegate {
self.startNoChatting() self.startNoChatting()
} }
} }
do {
let jsonDecoder = JSONDecoder()
let decoded = try jsonDecoder.decode(LiveRoomChatRawMessage.self, from: rawMessage.rawData)
let (nickname, profileUrl) = getUserNicknameAndProfileUrl(accountId: Int(peerId)!)
if decoded.type == .SECRET_DONATION {
self.messages.append(
LiveRoomDonationChat(
profileUrl: profileUrl,
nickname: nickname,
chat: decoded.message,
can: decoded.can,
donationMessage: decoded.donationMessage ?? ""
)
)
if let signature = decoded.signature {
self.addSignature(signature: signature)
} else if let imageUrl = decoded.signatureImageUrl {
self.addSignatureImage(imageUrl: imageUrl)
}
}
} catch {
}
} }
} }
} }
@ -1864,11 +2016,12 @@ extension LiveRoomViewModel: AgoraRtmChannelDelegate {
} catch { } catch {
} }
} else { } else {
let memberId = Int(member.userId) ?? 0
let chat = message.text let chat = message.text
let rank = getUserRank(userId: Int(member.userId) ?? 0) let rank = getUserRank(userId: memberId)
if !chat.trimmingCharacters(in: .whitespaces).isEmpty { if !chat.trimmingCharacters(in: .whitespaces).isEmpty && !blockedMemberIdList.contains(memberId) {
messages.append(LiveRoomNormalChat(userId: Int(member.userId)!, profileUrl: profileUrl, nickname: nickname, rank: rank, chat: chat)) messages.append(LiveRoomNormalChat(userId: memberId, profileUrl: profileUrl, nickname: nickname, rank: rank, chat: chat))
} }
} }

View File

@ -145,7 +145,7 @@ final class RouletteSettingsViewModel: ObservableObject {
} }
} }
if totalPercentage != Float(100) { if totalPercentage > Float(100.1) || totalPercentage <= Float(99.99) {
isLoading = false isLoading = false
errorMessage = "확률이 100%가 아닙니다" errorMessage = "확률이 100%가 아닙니다"
isShowErrorPopup = true isShowErrorPopup = true

View File

@ -30,6 +30,7 @@ struct LiveRoomInfoGuestView: View {
let onClickQuit: () -> Void let onClickQuit: () -> Void
let onClickToggleBg: () -> Void let onClickToggleBg: () -> Void
let onClickShare: () -> Void
let onClickFollow: (Bool) -> Void let onClickFollow: (Bool) -> Void
let onClickProfile: (Int) -> Void let onClickProfile: (Int) -> Void
let onClickNotice: () -> Void let onClickNotice: () -> Void
@ -85,6 +86,13 @@ struct LiveRoomInfoGuestView: View {
strokeWidth: 1, strokeWidth: 1,
strokeCornerRadius: 5.3 strokeCornerRadius: 5.3
) { onClickToggleBg() } ) { onClickToggleBg() }
LiveRoomOverlayStrokeImageButton(
imageName: "ic_share",
strokeColor: Color.graybb,
strokeWidth: 1,
strokeCornerRadius: 5.3
) { onClickShare() }
} }
HStack(spacing: 8) { HStack(spacing: 8) {
@ -211,6 +219,24 @@ struct LiveRoomInfoGuestView_Previews: PreviewProvider {
profileImage: "https://cf.sodalive.net/profile/4679/4679-profile-41e83399-234e-4541-8591-f961a025cfaa-5819-1699536915310", profileImage: "https://cf.sodalive.net/profile/4679/4679-profile-41e83399-234e-4541-8591-f961a025cfaa-5819-1699536915310",
role: .SPEAKER role: .SPEAKER
), ),
LiveRoomMember(
id: 4,
nickname: "도화",
profileImage: "https://cf.sodalive.net/profile/26/26-profile-ddf78b4d-0300-4c50-9c84-5d8a95fd5fe2-4892-1705256364320",
role: .SPEAKER
),
LiveRoomMember(
id: 5,
nickname: "도화",
profileImage: "https://cf.sodalive.net/profile/26/26-profile-ddf78b4d-0300-4c50-9c84-5d8a95fd5fe2-4892-1705256364320",
role: .SPEAKER
),
LiveRoomMember(
id: 6,
nickname: "도화",
profileImage: "https://cf.sodalive.net/profile/26/26-profile-ddf78b4d-0300-4c50-9c84-5d8a95fd5fe2-4892-1705256364320",
role: .SPEAKER
),
], ],
muteSpeakerList: [], muteSpeakerList: [],
activeSpeakerList: [], activeSpeakerList: [],
@ -218,6 +244,7 @@ struct LiveRoomInfoGuestView_Previews: PreviewProvider {
isAdult: false, isAdult: false,
onClickQuit: {}, onClickQuit: {},
onClickToggleBg: {}, onClickToggleBg: {},
onClickShare: {},
onClickFollow: { _ in }, onClickFollow: { _ in },
onClickProfile: { _ in }, onClickProfile: { _ in },
onClickNotice: {}, onClickNotice: {},

View File

@ -31,6 +31,7 @@ struct LiveRoomInfoHostView: View {
let onClickQuit: () -> Void let onClickQuit: () -> Void
let onClickToggleBg: () -> Void let onClickToggleBg: () -> Void
let onClickShare: () -> Void
let onClickEdit: () -> Void let onClickEdit: () -> Void
let onClickProfile: (Int) -> Void let onClickProfile: (Int) -> Void
let onClickNotice: () -> Void let onClickNotice: () -> Void
@ -77,6 +78,13 @@ struct LiveRoomInfoHostView: View {
strokeCornerRadius: 5.3 strokeCornerRadius: 5.3
) { onClickToggleBg() } ) { onClickToggleBg() }
LiveRoomOverlayStrokeImageButton(
imageName: "ic_share",
strokeColor: Color.graybb,
strokeWidth: 1,
strokeCornerRadius: 5.3
) { onClickShare() }
LiveRoomOverlayStrokeImageButton( LiveRoomOverlayStrokeImageButton(
imageName: "ic_edit", imageName: "ic_edit",
strokeColor: Color.graybb, strokeColor: Color.graybb,
@ -227,12 +235,31 @@ struct LiveRoomInfoHostView_Previews: PreviewProvider {
profileImage: "https://cf.sodalive.net/profile/4679/4679-profile-41e83399-234e-4541-8591-f961a025cfaa-5819-1699536915310", profileImage: "https://cf.sodalive.net/profile/4679/4679-profile-41e83399-234e-4541-8591-f961a025cfaa-5819-1699536915310",
role: .SPEAKER role: .SPEAKER
), ),
LiveRoomMember(
id: 4,
nickname: "도화",
profileImage: "https://cf.sodalive.net/profile/26/26-profile-ddf78b4d-0300-4c50-9c84-5d8a95fd5fe2-4892-1705256364320",
role: .SPEAKER
),
LiveRoomMember(
id: 5,
nickname: "도화",
profileImage: "https://cf.sodalive.net/profile/26/26-profile-ddf78b4d-0300-4c50-9c84-5d8a95fd5fe2-4892-1705256364320",
role: .SPEAKER
),
LiveRoomMember(
id: 6,
nickname: "도화",
profileImage: "https://cf.sodalive.net/profile/26/26-profile-ddf78b4d-0300-4c50-9c84-5d8a95fd5fe2-4892-1705256364320",
role: .SPEAKER
),
], ],
muteSpeakerList: [], muteSpeakerList: [],
activeSpeakerList: [], activeSpeakerList: [],
isAdult: false, isAdult: false,
onClickQuit: {}, onClickQuit: {},
onClickToggleBg: {}, onClickToggleBg: {},
onClickShare: {},
onClickEdit: {}, onClickEdit: {},
onClickProfile: { _ in }, onClickProfile: { _ in },
onClickNotice: {}, onClickNotice: {},

View File

@ -45,6 +45,9 @@ struct LiveRoomViewV2: View {
onClickToggleBg: { onClickToggleBg: {
viewModel.isBgOn.toggle() viewModel.isBgOn.toggle()
}, },
onClickShare: {
viewModel.shareRoom()
},
onClickEdit: { onClickEdit: {
viewModel.isShowEditRoomInfoDialog = true viewModel.isShowEditRoomInfoDialog = true
}, },
@ -92,6 +95,9 @@ struct LiveRoomViewV2: View {
onClickToggleBg: { onClickToggleBg: {
viewModel.isBgOn.toggle() viewModel.isBgOn.toggle()
}, },
onClickShare: {
viewModel.shareRoom()
},
onClickFollow: { onClickFollow: {
if $0 { if $0 {
viewModel.creatorUnFollow() viewModel.creatorUnFollow()
@ -145,16 +151,18 @@ struct LiveRoomViewV2: View {
ScrollView(.vertical, showsIndicators: false) { ScrollView(.vertical, showsIndicators: false) {
scrollObservableView scrollObservableView
LiveRoomChatView(messages: viewModel.messages) { if !viewModel.changeIsAdult || UserDefaults.bool(forKey: .auth) {
if $0 != UserDefaults.int(forKey: .userId) { LiveRoomChatView(messages: viewModel.messages) {
viewModel.getUserProfile(userId: $0) if $0 != UserDefaults.int(forKey: .userId) {
viewModel.getUserProfile(userId: $0)
}
} }
} .frame(width: screenSize().width)
.frame(width: screenSize().width) .rotationEffect(Angle(degrees: 180))
.rotationEffect(Angle(degrees: 180)) .valueChanged(value: viewModel.messageChangeFlag) { _ in
.valueChanged(value: viewModel.messageChangeFlag) { _ in if viewModel.offset - viewModel.originOffset > (56.7 * 2) {
if viewModel.offset - viewModel.originOffset > (56.7 * 2) { viewModel.isShowingNewChat = true
viewModel.isShowingNewChat = true }
} }
} }
} }
@ -197,13 +205,12 @@ struct LiveRoomViewV2: View {
) )
} }
if liveRoomInfo.creatorId == UserDefaults.int(forKey: .userId) && LiveRoomRightBottomButton(
UserDefaults.string(forKey: .role) == MemberRole.CREATOR.rawValue { imageName: "ic_donation_message_list",
LiveRoomRightBottomButton( onClick: { viewModel.isShowDonationMessagePopup = true }
imageName: "ic_donation_message_list", )
onClick: { viewModel.isShowDonationMessagePopup = true }
) if liveRoomInfo.creatorId != UserDefaults.int(forKey: .userId) {
} else {
LiveRoomRightBottomButton( LiveRoomRightBottomButton(
imageName: "ic_donation", imageName: "ic_donation",
onClick: { viewModel.isShowDonationPopup = true } onClick: { viewModel.isShowDonationPopup = true }
@ -353,6 +360,7 @@ struct LiveRoomViewV2: View {
viewModel.getMemberCan() viewModel.getMemberCan()
viewModel.initAgoraEngine() viewModel.initAgoraEngine()
viewModel.getRoomInfo() viewModel.getRoomInfo()
viewModel.getBlockedMemberIdList()
NotificationCenter.default.addObserver( NotificationCenter.default.addObserver(
forName: UIApplication.willTerminateNotification, forName: UIApplication.willTerminateNotification,
@ -387,11 +395,22 @@ struct LiveRoomViewV2: View {
} }
if viewModel.isShowDonationPopup { if viewModel.isShowDonationPopup {
LiveRoomDonationDialogView(isShowing: $viewModel.isShowDonationPopup, isAudioContentDonation: false) { can, message in LiveRoomDonationDialogView(isShowing: $viewModel.isShowDonationPopup, isAudioContentDonation: false) { can, message, isSecret in
viewModel.donation(can: can, message: message) viewModel.donation(can: can, message: message, isSecret: isSecret)
} }
} }
if viewModel.changeIsAdult && !UserDefaults.bool(forKey: .auth) {
SodaDialog(
title: "알림",
desc: "지금 참여하던 라이브는 '19세 이상' 연령제한이 설정되어 정보통신망 이용촉진 및 정보 보호 등에 관한 법률 및 청소년 보호법의 규정에 의해 만 19세 미만의 청소년은 이용할 수 없습니다.\n마이페이지에서 본인인증 후 다시 이용하시기 바랍니다.",
confirmButtonTitle: "확인",
confirmButtonAction: {
viewModel.quitRoom()
}
)
}
if viewModel.isShowQuitPopup { if viewModel.isShowQuitPopup {
SodaDialog( SodaDialog(
title: "라이브 나가기", title: "라이브 나가기",
@ -515,7 +534,7 @@ struct LiveRoomViewV2: View {
) )
Rectangle() Rectangle()
.foregroundColor(Color(hex: "222222")) .foregroundColor(Color.gray22)
.frame(width: screenSize().width, height: 15.3) .frame(width: screenSize().width, height: 15.3)
} }
.ignoresSafeArea() .ignoresSafeArea()
@ -640,15 +659,17 @@ struct LiveRoomViewV2: View {
isShowing: $viewModel.isShowEditRoomInfoDialog, isShowing: $viewModel.isShowEditRoomInfoDialog,
isShowPhotoPicker: $viewModel.isShowPhotoPicker, isShowPhotoPicker: $viewModel.isShowPhotoPicker,
viewModel: viewModel, viewModel: viewModel,
isAdult: liveRoomInfo.isAdult,
isLoading: viewModel.isLoading, isLoading: viewModel.isLoading,
currentTitle: liveRoomInfo.title, currentTitle: liveRoomInfo.title,
currentNotice: liveRoomInfo.notice, currentNotice: liveRoomInfo.notice,
coverImageUrl: liveRoomInfo.coverImageUrl, coverImageUrl: liveRoomInfo.coverImageUrl,
coverImage: viewModel.coverImage coverImage: viewModel.coverImage
) { newTitle, newNotice in ) { newTitle, newNotice, isAdult in
self.viewModel.editLiveRoomInfo( self.viewModel.editLiveRoomInfo(
title: newTitle, title: newTitle,
notice: newNotice notice: newNotice,
isAdult: isAdult
) )
} }
} else { } else {
@ -662,7 +683,7 @@ struct LiveRoomViewV2: View {
LiveRoomDonationRankingDialog(isShowing: $viewModel.isShowDonationRankingPopup) LiveRoomDonationRankingDialog(isShowing: $viewModel.isShowDonationRankingPopup)
} }
.sheet(isPresented: $viewModel.isShowDonationMessagePopup) { .sheet(isPresented: $viewModel.isShowDonationMessagePopup) {
LiveRoomDonationMessageDialog(isShowing: $viewModel.isShowDonationMessagePopup) LiveRoomDonationMessageDialog(viewModel: viewModel, isShowing: $viewModel.isShowDonationMessagePopup)
} }
} }
@ -674,7 +695,7 @@ struct LiveRoomViewV2: View {
} }
private func inviteSpeaker(peerId: Int) { private func inviteSpeaker(peerId: Int) {
if viewModel.liveRoomInfo!.speakerList.count <= 4 { if viewModel.liveRoomInfo!.speakerList.count <= 5 {
viewModel.inviteSpeaker(peerId: peerId) viewModel.inviteSpeaker(peerId: peerId)
self.viewModel.popupContent = "스피커 요청을 보냈습니다.\n잠시만 기다려 주세요." self.viewModel.popupContent = "스피커 요청을 보냈습니다.\n잠시만 기다려 주세요."
self.viewModel.isShowPopup = true self.viewModel.isShowPopup = true

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