Compare commits

..

64 Commits

Author SHA1 Message Date
Yu Sung 115a30a7b6 채금 시간이 설정 되지 않던 버그 수정 2023-10-15 07:28:00 +09:00
Yu Sung c78b804678 탐색 - 인기 크리에이터 설명 글 UI 수정 2023-10-15 06:53:47 +09:00
Yu Sung 1a01244d85 탐색 - 인기 크리에이터 설명 글 UI 수정 2023-10-15 06:40:09 +09:00
Yu Sung fea557560c 인기콘텐츠 전체 보기 페이지 추가 2023-10-15 06:34:08 +09:00
Yu Sung c440e8abd9 콘텐츠 메인 - 인기 콘텐츠 영역 추가 2023-10-15 05:50:54 +09:00
Yu Sung 41d5bad46f 탐색 - 크리에이터 랭킹 UI 추가 2023-10-14 18:34:10 +09:00
Yu Sung 6c8e19aed5 Admob 제거 2023-10-14 17:31:28 +09:00
Yu Sung 282ee73de1 채금 기능 추가 2023-10-11 19:25:12 +09:00
Yu Sung 91c43e679f 라이브 예약중 - 예약중 글자색, 내가 개설한 라이브 글자색 3bb9f1로 변경 2023-10-06 20:31:13 +09:00
Yu Sung 02835c4b2e 라이브 예약 중 전체보기 - 선택된 날짜 배경색 3bb9f1로 변경 2023-10-06 20:28:02 +09:00
Yu Sung a4d15be57a 메시지 발송 버튼 색 변경, 입력창 커서 색 변경 2023-10-06 20:26:12 +09:00
Yu Sung 219128ea8d 라이브 방 - 팔로우 버튼 위치 수정 2023-10-06 20:19:16 +09:00
Yu Sung 47ae2ec8e1 유료 콘텐츠 미리 듣기 재생 버튼 추가 2023-10-06 01:22:56 +09:00
Yu Sung 962197d319 후원랭킹 전체보기 - 채널에 후원랭킹 활성화 스위치 추가 2023-10-06 01:02:32 +09:00
Yu Sung c75f94722b 배경색 2b2635 -> 13181b 로 변경 2023-10-05 23:35:45 +09:00
Yu Sung c00931761c 라이브 상세 - 공유 버튼 제거 2023-10-05 02:03:41 +09:00
Yu Sung 8255065bba 비공개 라이브 입장 - 비밀번호 입력창 나오지 않던 버그 수정 2023-10-04 20:25:19 +09:00
Yu Sung 1e1b97e2d4 새로운 콘텐츠 전체보기, 큐레이션 전체보기 - grid item alignment top으로 설정 2023-09-27 22:17:47 +09:00
Yu Sung f653667df2 새로운 콘텐츠 전체보기, 콘텐츠 큐레이션 전체보기 - 스크롤 로딩 추가 2023-09-27 21:22:26 +09:00
Yu Sung 658cff20eb 큐레이션 전체보기 페이지 추가 2023-09-27 21:16:39 +09:00
Yu Sung fd356451ae 새로운 콘텐츠 전체보기 페이지 추가 2023-09-27 19:25:00 +09:00
Yu Sung 91cd3fe995 콘텐츠 업로드 - 미리듣기 시간 설정 안내 문구 추가 2023-09-27 18:13:15 +09:00
Yu Sung 5f7924880e 콘텐츠 업로드 - 미리 듣기 시간 타입 옵셔널로 수정 2023-09-23 00:57:38 +09:00
Yu Sung 00a8f7e8ff 푸시토큰 잘못 불러오는 버그 수정 2023-09-23 00:29:26 +09:00
Yu Sung d1b5ab31aa 콘텐츠 업로드 - 미리 듣기 시간 설정 기능 추가 2023-09-23 00:08:23 +09:00
Yu Sung 225efc34e4 라이브 방 후원 다이얼로그 - 캔 불러오는 방식 수정 2023-09-21 23:03:28 +09:00
Yu Sung 4e607ed624 라이브 상세 - 상단에 배너 광고 추가 2023-09-21 19:01:27 +09:00
Yu Sung c7522aa7c9 .. 2023-09-19 15:37:28 +09:00
Yu Sung b632a65a6f 캔 유효기간 설명 제거 2023-09-18 10:46:31 +09:00
Yu Sung d3c5a5bfb9 콘텐츠 상세 - 배너 광고 간격 수정 2023-09-16 01:41:21 +09:00
Yu Sung 2449eb14d0 콘텐츠 상세 - 배너 광고 위치 수정 2023-09-16 01:31:11 +09:00
Yu Sung 4c9e78f960 탐색 - 배너 광고 추가 2023-09-16 01:11:50 +09:00
Yu Sung 6e9aaa0c8a 콘텐츠 메인 - 배너 광고 위치 수정 2023-09-16 00:53:59 +09:00
Yu Sung 0294bbf223 라이브 - 배너 광고 위치 변경 2023-09-16 00:10:58 +09:00
Yu Sung 36aa167e1d 라이브 방 - 배너 광고 제거 2023-09-15 23:36:21 +09:00
Yu Sung 8c20ce9c62 라이브 방 - 배너 광고 추가 2023-09-15 18:27:53 +09:00
Yu Sung f44ef505cf 크리에이터 프로필 - 배너 광고 추가 2023-09-15 18:11:40 +09:00
Yu Sung 9a72e21fda 팔로잉 채널 리스트 - 배너 광고 추가 2023-09-15 18:03:46 +09:00
Yu Sung 1c4503bda1 콘텐츠 메인 - 배너 광고 추가 2023-09-15 17:59:23 +09:00
Yu Sung b1773b117d 라이브, 지금 라이브 중 전체보기 - 배너 광고 추가 2023-09-15 17:03:16 +09:00
Yu Sung a16c38f4ab 메시지 - 배너 광고 추가 2023-09-15 16:28:11 +09:00
Yu Sung 475882570a 구매목록 - 배너광고 추가 2023-09-15 16:04:21 +09:00
Yu Sung 5b0cb44645 contentShape 추가 2023-09-14 12:50:58 +09:00
Yu Sung b2f0975ad1 콘텐츠 상세 - 배너 광고 추가 2023-09-14 12:16:01 +09:00
Yu Sung 48b1093dac 콘텐츠 답글 - 댓글이 랜덤으로 보이는 버그 수정 2023-09-13 16:54:24 +09:00
Yu Sung 3bcd2b7dba 메시지 추가 로딩 되지 않는 버그 수정 2023-09-13 14:39:35 +09:00
Yu Sung 9863fc66de 재생 수 업데이트 로직 추가 2023-09-13 12:14:03 +09:00
Yu Sung 58868f613a 푸시 터치 액션 수정 2023-09-13 11:45:41 +09:00
Yu Sung 1f992a11dc 응원글 전체보기 - 응원글 삭제기능 추가 2023-09-09 00:38:55 +09:00
Yu Sung 948b1fd2b3 응원글 수정 기능 추가 2023-09-09 00:10:22 +09:00
Yu Sung 5d95c0f1c9 응원글 삭제 기능 추가 2023-09-09 00:02:39 +09:00
Yu Sung b31933715d 콘텐츠 댓글 - 수정/삭제 추가 2023-09-08 19:33:09 +09:00
Yu Sung 707b6f804c 구매목록 - 콘텐츠 크리에이터 표시 2023-09-04 23:10:34 +09:00
Yu Sung 05f5a4fe82 라이브 생성 푸시 - 예약중인 라이브의 경우 상세 페이지가 나오도록 수정 2023-09-04 19:03:02 +09:00
Yu Sung f08d72745e 음성메시지 보관/삭제/답장 추가 2023-09-01 23:09:36 +09:00
Yu Sung 4611524f8f 라이브 예약이 되지 않던 버그 수정 2023-09-01 12:13:30 +09:00
Yu Sung a41c423991 푸시, 딥링크 - 라이브 탭으로 이동하지 않아도 실행되도록 수정 2023-09-01 00:35:50 +09:00
Yu Sung d76b1c7a59 라이브 - 상단 배너 터치 액션 추가 2023-08-30 22:05:23 +09:00
Yu Sung 6fa183b89a 코인 -> 캔 2023-08-29 23:38:01 +09:00
Yu Sung 371d6d538a 콘텐츠 대여가격 60%로 변경 2023-08-29 23:29:05 +09:00
Yu Sung 2aa3f944c8 응원 전체보기 추가 2023-08-29 15:30:21 +09:00
Yu Sung e68961bd0d 후원랭킹 전체보기 추가 2023-08-29 14:10:16 +09:00
Yu Sung 88fcbc98f4 스피커 요청 버튼 제거 2023-08-28 23:13:43 +09:00
Yu Sung 3916a49e60 콘텐츠 등록 - 5캔(500원) 부터 등록되도록 수정, 대여가격 안내 60%로 수정 2023-08-28 18:20:58 +09:00
117 changed files with 4042 additions and 770 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 978 B

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 845 B

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 927 B

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

View File

@ -1,25 +1,226 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>FirebaseAppDelegateProxyEnabled</key> <key>FirebaseAppDelegateProxyEnabled</key>
<false/> <false/>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
<true/> <true/>
</dict> </dict>
<key>UIAppFonts</key> <key>UIAppFonts</key>
<array> <array>
<string>gmarket_sans_bold.otf</string> <string>gmarket_sans_bold.otf</string>
<string>gmarket_sans_medium.otf</string> <string>gmarket_sans_medium.otf</string>
<string>gmarket_sans_light.otf</string> <string>gmarket_sans_light.otf</string>
</array> </array>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>audio</string> <string>audio</string>
<string>fetch</string> <string>fetch</string>
<string>remote-notification</string> <string>remote-notification</string>
</array> </array>
</dict> <key>GADApplicationIdentifier</key>
<string>ca-app-pub-1299501215847962~3447556960</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>
</plist> </plist>

View File

@ -21,5 +21,206 @@
<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

@ -16,7 +16,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure() FirebaseApp.configure()
Messaging.messaging().delegate = self Messaging.messaging().delegate = self
// For iOS 10 display notification (sent via APNS) // For iOS 10 display notification (sent via APNS)
@ -95,16 +94,26 @@ extension AppDelegate : UNUserNotificationCenterDelegate {
Messaging.messaging().appDidReceiveMessage(userInfo) Messaging.messaging().appDidReceiveMessage(userInfo)
let roomIdString = userInfo["room_id"] as? String let roomIdString = userInfo["room_id"] as? String
let audioContentIdString = userInfo["audio_content_id"] as? String let contentIdString = userInfo["content_id"] as? String
let channelIdString = userInfo["channel_id"] as? String
let messageIdString = userInfo["message_id"] as? String
if let roomIdString = roomIdString, let roomId = Int(roomIdString), roomId > 0 { if let roomIdString = roomIdString, let roomId = Int(roomIdString), roomId > 0 {
AppState.shared.pushRoomId = roomId AppState.shared.pushRoomId = roomId
} }
if let audioContentIdString = audioContentIdString, let audioContentId = Int(audioContentIdString), audioContentId > 0 { if let contentIdString = contentIdString, let audioContentId = Int(contentIdString), audioContentId > 0 {
AppState.shared.pushAudioContentId = audioContentId AppState.shared.pushAudioContentId = audioContentId
} }
if let channelIdString = channelIdString, let channelId = Int(channelIdString), channelId > 0 {
AppState.shared.pushChannelId = channelId
}
if let messageIdString = messageIdString, let messageId = Int(messageIdString), messageId > 0 {
AppState.shared.pushMessageId = messageId
}
completionHandler() completionHandler()
} }
} }

View File

@ -26,6 +26,7 @@ class AppState: ObservableObject {
@Published var pushRoomId = 0 @Published var pushRoomId = 0
@Published var pushChannelId = 0 @Published var pushChannelId = 0
@Published var pushMessageId = 0
@Published var pushAudioContentId = 0 @Published var pushAudioContentId = 0
@Published var roomId = 0 { @Published var roomId = 0 {
didSet { didSet {

View File

@ -107,4 +107,10 @@ enum AppStep {
case followingList case followingList
case orderListAll case orderListAll
case newContentAll
case curationAll(title: String, curationId: Int)
case contentRankingAll
} }

View File

@ -26,7 +26,8 @@ struct SodaLiveApp: App {
let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems
let roomId = queryItems?.filter({$0.name == "room_id"}).first?.value let roomId = queryItems?.filter({$0.name == "room_id"}).first?.value
let channelId = queryItems?.filter({$0.name == "channel_id"}).first?.value let channelId = queryItems?.filter({$0.name == "channel_id"}).first?.value
let audioContentId = queryItems?.filter({$0.name == "audio_content_id"}).first?.value let messageId = queryItems?.filter({$0.name == "message_id"}).first?.value
let audioContentId = queryItems?.filter({$0.name == "content_id"}).first?.value
if let roomId = roomId { if let roomId = roomId {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
@ -40,6 +41,12 @@ struct SodaLiveApp: App {
} }
} }
if let messageId = messageId {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
AppState.shared.pushMessageId = Int(messageId) ?? 0
}
}
if let audioContentId = audioContentId { if let audioContentId = audioContentId {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
AppState.shared.pushAudioContentId = Int(audioContentId) ?? 0 AppState.shared.pushAudioContentId = Int(audioContentId) ?? 0

View File

@ -13,7 +13,7 @@ struct AddAllPlaybackTrackingRequest: Encodable {
} }
struct PlaybackTrackingData: Encodable { struct PlaybackTrackingData: Encodable {
let audioContentId: Int let contentId: Int
let playDateTime: String let playDateTime: String
let isPreview: Bool let isPreview: Bool
} }

View File

@ -0,0 +1,54 @@
//
// ContentNewAllItemView.swift
// SodaLive
//
// Created by klaus on 2023/09/27.
//
import SwiftUI
import Kingfisher
struct ContentNewAllItemView: View {
let item: GetAudioContentMainItem
@State var width: CGFloat = 0
var body: some View {
VStack(alignment: .leading, spacing: 8) {
KFImage(URL(string: item.coverImageUrl))
.resizable()
.scaledToFill()
.frame(width: width, height: width, alignment: .top)
.cornerRadius(2.7)
Text(item.title)
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "d2d2d2"))
.frame(width: width, alignment: .leading)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(2)
HStack(spacing: 5.3) {
KFImage(URL(string: item.creatorProfileImageUrl))
.resizable()
.scaledToFill()
.frame(width: 21.3, height: 21.3)
.clipShape(Circle())
.onTapGesture { AppState.shared.setAppStep(step: .creatorDetail(userId: item.creatorId)) }
Text(item.creatorNickname)
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "777777"))
.lineLimit(1)
}
.padding(.bottom, 10)
}
.frame(width: width)
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: item.contentId)) }
.onAppear {
width = (screenSize().width - 40) / 2
}
}
}

View File

@ -0,0 +1,86 @@
//
// ContentNewAllView.swift
// SodaLive
//
// Created by klaus on 2023/09/27.
//
import SwiftUI
struct ContentNewAllView: View {
@StateObject var viewModel = ContentNewAllViewModel()
let columns = [
GridItem(.flexible(), alignment: .top),
GridItem(.flexible(), alignment: .top)
]
var body: some View {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) {
DetailNavigationBar(title: "새로운 콘텐츠")
Text("※ 최근 2주간 등록된 새로운 콘텐츠 입니다.")
.font(.custom(Font.medium.rawValue, size: 14.7))
.foregroundColor(Color(hex: "bbbbbb"))
.padding(.horizontal, 13.3)
.padding(.vertical, 8)
.frame(width: screenSize().width, alignment: .leading)
.background(Color(hex: "222222"))
.padding(.top, 13.3)
ContentMainNewContentThemeView(
themes: viewModel.themeList,
selectTheme: {
viewModel.selectedTheme = $0
},
selectedTheme: $viewModel.selectedTheme
).padding(.top, 13.3)
HStack(spacing: 0) {
Text("전체")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "e2e2e2"))
Text("\(viewModel.totalCount)")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "ff5c49"))
.padding(.leading, 8)
Text("")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "e2e2e2"))
.padding(.leading, 2)
Spacer()
}
.padding(.vertical, 13.3)
.padding(.horizontal, 20)
ScrollView(.vertical, showsIndicators: false) {
LazyVGrid(columns: columns, spacing: 13.3) {
ForEach(0..<viewModel.newContentList.count, id: \.self) { index in
ContentNewAllItemView(item: viewModel.newContentList[index])
.onAppear {
if index == viewModel.newContentList.count - 1 {
viewModel.getNewContentList()
}
}
}
}
}
}
.onAppear {
viewModel.getThemeList()
viewModel.getNewContentList()
}
}
}
}
struct ContentNewAllView_Previews: PreviewProvider {
static var previews: some View {
ContentNewAllView()
}
}

View File

@ -0,0 +1,130 @@
//
// ContentNewAllViewModel.swift
// SodaLive
//
// Created by klaus on 2023/09/27.
//
import Foundation
import Combine
final class ContentNewAllViewModel: ObservableObject {
private let repository = ContentRepository()
private var subscription = Set<AnyCancellable>()
@Published var errorMessage = ""
@Published var isShowPopup = false
@Published var isLoading = false
@Published var themeList = [String]()
@Published var newContentList = [GetAudioContentMainItem]()
@Published var selectedTheme = "전체" {
didSet {
page = 1
isLast = false
getNewContentList()
}
}
@Published var totalCount = 0
var page = 1
var isLast = false
private let pageSize = 10
func getNewContentList() {
if (!isLast && !isLoading) {
isLoading = true
repository.getNewContentAllOfTheme(
theme: selectedTheme == "전체" ? "" : selectedTheme,
page: page,
size: pageSize
)
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { [unowned self] response in
self.isLoading = false
let responseData = response.data
do {
let jsonDecoder = JSONDecoder()
let decoded = try jsonDecoder.decode(ApiResponse<GetNewContentAllResponse>.self, from: responseData)
self.isLoading = false
if let data = decoded.data, decoded.success {
if page == 1 {
newContentList.removeAll()
}
self.totalCount = data.totalCount
if !data.items.isEmpty {
page += 1
self.newContentList.append(contentsOf: data.items)
} else {
isLast = true
}
} else {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
}
self.isShowPopup = true
}
} catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
self.isLoading = false
}
}
.store(in: &subscription)
} else {
isLoading = false
}
}
func getThemeList() {
repository.getNewContentThemeList()
.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<[String]>.self, from: responseData)
if let data = decoded.data, decoded.success {
self.themeList.append("전체")
self.themeList.append(contentsOf: data)
} else {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
}
self.isShowPopup = true
}
} catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
}
}
.store(in: &subscription)
}
}

View File

@ -0,0 +1,153 @@
//
// ContentRankingAllView.swift
// SodaLive
//
// Created by klaus on 2023/10/15.
//
import SwiftUI
import Kingfisher
struct ContentRankingAllView: View {
@StateObject var viewModel = ContentRankingAllViewModel()
var body: some View {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) {
DetailNavigationBar(title: "인기 콘텐츠")
VStack(spacing: 8) {
Text("\(viewModel.dateString)")
.font(.custom(Font.bold.rawValue, size: 14.7))
.foregroundColor(Color(hex: "eeeeee"))
Text("※ 인기 콘텐츠의 순위는 매주 업데이트됩니다.")
.font(.custom(Font.light.rawValue, size: 13.3))
.foregroundColor(Color(hex: "bbbbbb"))
}
.padding(.vertical, 8)
.frame(width: screenSize().width - 26.7)
.background(Color(hex: "222222"))
.padding(.top, 13.3)
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 20) {
ForEach(0..<viewModel.contentRankingItemList.count, id: \.self) { index in
let item = viewModel.contentRankingItemList[index]
HStack(spacing: 0) {
KFImage(URL(string: item.coverImageUrl))
.resizable()
.scaledToFill()
.frame(width: 66.7, height: 66.7, alignment: .top)
.clipped()
.cornerRadius(5.3)
Text("\(index + 1)")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "3bb9f1"))
.padding(.horizontal, 12)
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 8) {
Text(item.themeStr)
.font(.custom(Font.medium.rawValue, size: 8))
.foregroundColor(Color(hex: "3bac6a"))
.padding(2.6)
.background(Color(hex: "28312b"))
.cornerRadius(2.6)
Text(item.duration)
.font(.custom(Font.medium.rawValue, size: 8))
.foregroundColor(Color(hex: "777777"))
.padding(2.6)
.background(Color(hex: "222222"))
.cornerRadius(2.6)
}
Text(item.creatorNickname)
.font(.custom(Font.medium.rawValue, size: 10.7))
.foregroundColor(Color(hex: "777777"))
.padding(.vertical, 8)
Text(item.title)
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "d2d2d2"))
.lineLimit(2)
.padding(.top, 2.7)
}
Spacer()
if item.price > 0 {
HStack(spacing: 8) {
Image("ic_can")
.resizable()
.frame(width: 17, height: 17)
Text("\(item.price)")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "909090"))
}
} else {
Text("무료")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "ffffff"))
.padding(.horizontal, 5.3)
.padding(.vertical, 2.7)
.background(Color(hex: "cf5c37"))
.cornerRadius(2.6)
}
}
.frame(height: 66.7)
.contentShape(Rectangle())
.onTapGesture {
AppState
.shared
.setAppStep(step: .contentDetail(contentId: item.contentId))
}
.onAppear {
if index == viewModel.contentRankingItemList.count - 1 {
viewModel.getContentRanking()
}
}
}
}
}
.padding(13.3)
}
if viewModel.isLoading {
LoadingView()
}
}
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
GeometryReader { geo in
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.padding(.horizontal, 6.7)
.frame(width: geo.size.width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color(hex: "9970ff"))
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
}
}
.onAppear {
viewModel.getContentRanking()
}
}
}
struct ContentRankingAllView_Previews: PreviewProvider {
static var previews: some View {
ContentRankingAllView()
}
}

View File

@ -0,0 +1,79 @@
//
// ContentRankingAllViewModel.swift
// SodaLive
//
// Created by klaus on 2023/10/15.
//
import Foundation
import Combine
final class ContentRankingAllViewModel: ObservableObject {
private let repository = ContentRepository()
private var subscription = Set<AnyCancellable>()
@Published var errorMessage = ""
@Published var isShowPopup = false
@Published var isLoading = false
@Published var dateString = ""
@Published var contentRankingItemList = [GetAudioContentRankingItem]()
var page = 1
var isLast = false
private let pageSize = 10
func getContentRanking() {
if (!isLast && !isLoading && page <= 5) {
isLoading = true
repository.getContentRanking(page: page, size: pageSize)
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { [unowned self] response in
self.isLoading = false
let responseData = response.data
do {
let jsonDecoder = JSONDecoder()
let decoded = try jsonDecoder.decode(ApiResponse<GetAudioContentRanking>.self, from: responseData)
self.isLoading = false
if let data = decoded.data, decoded.success {
if page == 1 {
contentRankingItemList.removeAll()
}
dateString = "\(data.startDate)~\(data.endDate)"
if !data.items.isEmpty {
page += 1
self.contentRankingItemList.append(contentsOf: data.items)
} else {
isLast = true
}
} else {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
}
self.isShowPopup = true
}
} catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
self.isLoading = false
}
}
.store(in: &subscription)
}
}
}

View File

@ -0,0 +1,11 @@
//
// GetNewContentAllResponse.swift
// SodaLive
//
// Created by klaus on 2023/09/27.
//
struct GetNewContentAllResponse: Decodable {
let totalCount: Int
let items: [GetAudioContentMainItem]
}

View File

@ -25,6 +25,11 @@ enum ContentApi {
case getMain case getMain
case getNewContentOfTheme(theme: String) case getNewContentOfTheme(theme: String)
case donation(request: AudioContentDonationRequest) case donation(request: AudioContentDonationRequest)
case modifyComment(request: ModifyCommentRequest)
case getNewContentThemeList
case getNewContentAllOfTheme(theme: String, page: Int, size: Int)
case getAudioContentListByCurationId(curationId: Int, page: Int, size: Int, sort: ContentCurationViewModel.Sort)
case getContentRanking(page: Int, size: Int)
} }
extension ContentApi: TargetType { extension ContentApi: TargetType {
@ -81,15 +86,32 @@ extension ContentApi: TargetType {
case .donation: case .donation:
return "/audio-content/donation" return "/audio-content/donation"
case .modifyComment:
return "/audio-content/comment"
case .getNewContentThemeList:
return "/audio-content/main/theme"
case .getNewContentAllOfTheme:
return "/audio-content/main/new/all"
case .getAudioContentListByCurationId(let curationId, _, _, _):
return "/audio-content/curation/\(curationId)"
case .getContentRanking:
return "/audio-content/ranking"
} }
} }
var method: Moya.Method { var method: Moya.Method {
switch self { switch self {
case .getAudioContentList, .getAudioContentDetail, .getOrderList, .getAudioContentThemeList, .getAudioContentCommentList, .getAudioContentCommentReplyList, .getMain, .getNewContentOfTheme: case .getAudioContentList, .getAudioContentDetail, .getOrderList, .getAudioContentThemeList,
.getAudioContentCommentList, .getAudioContentCommentReplyList, .getMain, .getNewContentOfTheme,
.getNewContentThemeList, .getNewContentAllOfTheme, .getAudioContentListByCurationId, .getContentRanking:
return .get return .get
case .likeContent, .modifyAudioContent: case .likeContent, .modifyAudioContent, .modifyComment:
return .put return .put
case .registerComment, .orderAudioContent, .addAllPlaybackTracking, .uploadAudioContent, .donation: case .registerComment, .orderAudioContent, .addAllPlaybackTracking, .uploadAudioContent, .donation:
@ -173,6 +195,38 @@ extension ContentApi: TargetType {
case .donation(let request): case .donation(let request):
return .requestJSONEncodable(request) return .requestJSONEncodable(request)
case .modifyComment(let request):
return .requestJSONEncodable(request)
case .getNewContentThemeList:
return .requestPlain
case .getNewContentAllOfTheme(let theme, let page, let size):
let parameters = [
"theme": theme,
"page": page - 1,
"size": size
] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .getAudioContentListByCurationId(_, let page, let size, let sort):
let parameters = [
"page": page - 1,
"size": size,
"sort-type": sort
] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .getContentRanking(let page, let size):
let parameters = [
"page": page - 1,
"size": size,
] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
} }
} }

View File

@ -76,4 +76,24 @@ final class ContentRepository {
func donation(contentId: Int, can: Int, comment: String) -> AnyPublisher<Response, MoyaError> { func donation(contentId: Int, can: Int, comment: String) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.donation(request: AudioContentDonationRequest(contentId: contentId, donationCan: can, comment: comment))) return api.requestPublisher(.donation(request: AudioContentDonationRequest(contentId: contentId, donationCan: can, comment: comment)))
} }
func modifyComment(request: ModifyCommentRequest) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.modifyComment(request: request))
}
func getNewContentThemeList() -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getNewContentThemeList)
}
func getNewContentAllOfTheme(theme: String, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getNewContentAllOfTheme(theme: theme, page: page, size: size))
}
func getAudioContentListByCurationId(curationId: Int, page: Int, size: Int, sort: ContentCurationViewModel.Sort) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getAudioContentListByCurationId(curationId: curationId, page: page, size: size, sort: sort))
}
func getContentRanking(page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getContentRanking(page: page, size: size))
}
} }

View File

@ -221,7 +221,7 @@ struct ContentCreateView: View {
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 0) { HStack(spacing: 0) {
TextField("가격을 입력하세요(10캔 이상)", text: $viewModel.priceString) TextField("가격을 입력하세요(5캔 이상)", text: $viewModel.priceString)
.autocapitalization(.none) .autocapitalization(.none)
.disableAutocorrection(true) .disableAutocorrection(true)
.font(.custom(Font.bold.rawValue, size: 14.7)) .font(.custom(Font.bold.rawValue, size: 14.7))
@ -253,12 +253,12 @@ struct ContentCreateView: View {
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, 13.3) .padding(.top, 13.3)
Text("※ 대여가격은 소장가격의 70%로 자동 반영") Text("※ 대여가격은 소장가격의 60%로 자동 반영")
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color(hex: "777777"))
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
Text("※ 콘텐츠의 최소금액은 10캔 입니다") Text("※ 콘텐츠의 최소금액은 5캔 입니다")
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color(hex: "777777"))
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
@ -321,6 +321,61 @@ struct ContentCreateView: View {
.padding(.top, 26.7) .padding(.top, 26.7)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
VStack(spacing: 10) {
Text("미리듣기 시간 설정")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "eeeeee"))
.frame(maxWidth: .infinity, alignment: .leading)
Text("미리듣기 시간을 직접 설정하지 않으면 콘텐츠 앞부분 30초가 자동으로 설정됩니다. 미리듣기의 시간제한은 없습니다.")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "777777"))
.frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 13.3) {
VStack(spacing: 5.3) {
Text("시작 시간")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "d2d2d2"))
.frame(maxWidth: .infinity, alignment: .leading)
TextField("00:00:00", text: $viewModel.previewStartTime)
.autocapitalization(.none)
.disableAutocorrection(true)
.font(.custom(Font.bold.rawValue, size: 14.6))
.foregroundColor(Color(hex: "777777"))
.padding(.vertical, 16.7)
.padding(.horizontal, 13.3)
.background(Color(hex: "222222"))
.cornerRadius(6.7)
.keyboardType(.default)
.multilineTextAlignment(.center)
}
VStack(spacing: 5.3) {
Text("종료 시간")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "d2d2d2"))
.frame(maxWidth: .infinity, alignment: .leading)
TextField("00:00:30", text: $viewModel.previewEndTime)
.autocapitalization(.none)
.disableAutocorrection(true)
.font(.custom(Font.bold.rawValue, size: 14.6))
.foregroundColor(Color(hex: "777777"))
.padding(.vertical, 16.7)
.padding(.horizontal, 13.3)
.background(Color(hex: "222222"))
.cornerRadius(6.7)
.keyboardType(.default)
.multilineTextAlignment(.center)
}
}
.padding(.top, 3.3)
}
.padding(.top, 26.7)
.padding(.horizontal, 13.3)
VStack(spacing: 0) { VStack(spacing: 0) {
HStack(alignment: .top, spacing: 0) { HStack(alignment: .top, spacing: 0) {
Text("등록") Text("등록")

View File

@ -58,6 +58,9 @@ final class ContentCreateViewModel: ObservableObject {
} }
} }
@Published var previewStartTime: String = ""
@Published var previewEndTime: String = ""
var placeholder = "내용을 입력하세요" var placeholder = "내용을 입력하세요"
func uploadAudioContent() { func uploadAudioContent() {
@ -71,7 +74,9 @@ final class ContentCreateViewModel: ObservableObject {
price: price, price: price,
themeId: theme!.id, themeId: theme!.id,
isAdult: isAdult, isAdult: isAdult,
isCommentAvailable: isAvailableComment isCommentAvailable: isAvailableComment,
previewStartTime: previewStartTime.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 ? previewStartTime : nil,
previewEndTime: previewEndTime.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 ? previewEndTime : nil
) )
var multipartData = [MultipartFormData]() var multipartData = [MultipartFormData]()
@ -205,12 +210,68 @@ final class ContentCreateViewModel: ObservableObject {
return false return false
} }
if !isFree && price < 10 { if !isFree && price < 5 {
errorMessage = "콘텐츠의 최소금액은 10캔 입니다." errorMessage = "콘텐츠의 최소금액은 5캔 입니다."
isShowPopup = true isShowPopup = true
return false return false
} }
if previewStartTime.count > 0 && previewEndTime.count > 0 {
let startTimeArray = previewStartTime.split(separator: ":")
if startTimeArray.count != 3 {
errorMessage = "미리 듣기 시간 형식은 00:30:00 과 같아야 합니다"
isShowPopup = true
return false
}
for time in startTimeArray {
if time.count != 2 {
errorMessage = "미리 듣기 시간 형식은 00:30:00 과 같아야 합니다"
isShowPopup = true
return false
}
}
let endTimeArray = previewStartTime.split(separator: ":")
if endTimeArray.count != 3 {
errorMessage = "미리 듣기 시간 형식은 00:30:00 과 같아야 합니다"
isShowPopup = true
return false
}
for time in endTimeArray {
if time.count != 2 {
errorMessage = "미리 듣기 시간 형식은 00:30:00 과 같아야 합니다"
isShowPopup = true
return false
}
}
let timeDifference = timeDifference(startTime: previewStartTime, endTime: previewEndTime)
if timeDifference < 30.0 {
errorMessage = "미리 듣기의 최소 시간은 30초 입니다"
isShowPopup = true
return false
}
} else {
if previewStartTime.count > 0 || previewEndTime.count > 0 {
errorMessage = "미리 듣기 시작 시간과 종료 시간 둘 다 입력을 하거나 둘 다 입력 하지 않아야 합니다."
isShowPopup = true
return false
}
}
return true return true
} }
private func timeDifference(startTime: String, endTime: String) -> Double {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm:ss"
if let date1 = dateFormatter.date(from: startTime), let date2 = dateFormatter.date(from: endTime) {
return date2.timeIntervalSince(date1)
}
return 0
}
} }

View File

@ -15,4 +15,6 @@ struct CreateAudioContentRequest: Encodable {
let themeId: Int let themeId: Int
let isAdult: Bool let isAdult: Bool
let isCommentAvailable: Bool let isCommentAvailable: Bool
let previewStartTime: String?
let previewEndTime: String?
} }

View File

@ -0,0 +1,110 @@
//
// ContentCurationView.swift
// SodaLive
//
// Created by klaus on 2023/09/27.
//
import SwiftUI
struct ContentCurationView: View {
@StateObject var viewModel = ContentCurationViewModel()
let title: String
let curationId: Int
let columns = [
GridItem(.flexible(), alignment: .top),
GridItem(.flexible(), alignment: .top)
]
var body: some View {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) {
DetailNavigationBar(title: title)
HStack(spacing: 13.3) {
Spacer()
Text("최신순")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(
Color(hex: "e2e2e2")
.opacity(viewModel.sort == .NEWEST ? 1 : 0.5)
)
.onTapGesture {
if viewModel.sort != .NEWEST {
viewModel.sort = .NEWEST
}
}
Text("높은 가격순")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(
Color(hex: "e2e2e2")
.opacity(viewModel.sort == .PRICE_HIGH ? 1 : 0.5)
)
.onTapGesture {
if viewModel.sort != .PRICE_HIGH {
viewModel.sort = .PRICE_HIGH
}
}
Text("낮은 가격순")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(
Color(hex: "e2e2e2")
.opacity(viewModel.sort == .PRICE_LOW ? 1 : 0.5)
)
.onTapGesture {
if viewModel.sort != .PRICE_LOW {
viewModel.sort = .PRICE_LOW
}
}
}
.padding(.vertical, 13.3)
.padding(.horizontal, 20)
.background(Color(hex: "161616"))
.padding(.top, 13.3)
HStack(spacing: 0) {
Text("전체")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "e2e2e2"))
Text("\(viewModel.totalCount)")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "ff5c49"))
.padding(.leading, 8)
Text("")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "e2e2e2"))
.padding(.leading, 2)
Spacer()
}
.padding(.vertical, 13.3)
.padding(.horizontal, 20)
ScrollView(.vertical, showsIndicators: false) {
LazyVGrid(columns: columns, spacing: 13.3) {
ForEach(0..<viewModel.contentList.count, id: \.self) { index in
ContentNewAllItemView(item: viewModel.contentList[index])
.onAppear {
if index == viewModel.contentList.count - 1 {
viewModel.getContentList()
}
}
}
}
}
}
.onAppear {
viewModel.curationId = curationId
viewModel.getContentList()
}
}
}
}

View File

@ -0,0 +1,95 @@
//
// ContentCurationViewModel.swift
// SodaLive
//
// Created by klaus on 2023/09/27.
//
import Foundation
import Combine
final class ContentCurationViewModel: ObservableObject {
enum Sort: String {
case NEWEST, PRICE_HIGH, PRICE_LOW
}
private let repository = ContentRepository()
private var subscription = Set<AnyCancellable>()
@Published var errorMessage = ""
@Published var isShowPopup = false
@Published var isLoading = false
@Published var totalCount = 0
@Published var sort = Sort.NEWEST {
didSet {
page = 1
isLast = false
getContentList()
}
}
@Published var contentList: [GetAudioContentMainItem] = []
var curationId = 0
var page = 1
var isLast = false
private let pageSize = 10
func getContentList() {
if (!isLast && !isLoading) {
isLoading = true
repository.getAudioContentListByCurationId(
curationId: curationId,
page: page,
size: pageSize,
sort: sort
)
.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<GetCurationContentResponse>.self, from: responseData)
if let data = decoded.data, decoded.success {
if page == 1 {
self.contentList.removeAll()
}
if !data.items.isEmpty {
page += 1
self.totalCount = data.totalCount
self.contentList.append(contentsOf: data.items)
} else {
isLast = true
}
} else {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
}
self.isShowPopup = true
}
} catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
}
self.isLoading = false
}
.store(in: &subscription)
}
}
}

View File

@ -0,0 +1,11 @@
//
// GetCurationContentResponse.swift
// SodaLive
//
// Created by klaus on 2023/09/27.
//
struct GetCurationContentResponse: Decodable {
let totalCount: Int
let items: [GetAudioContentMainItem]
}

View File

@ -9,81 +9,168 @@ import SwiftUI
import Kingfisher import Kingfisher
struct AudioContentCommentItemView: View { struct AudioContentCommentItemView: View {
let contentCreatorId: Int
let comment: GetAudioContentCommentListItem let audioContentId: Int
let commentItem: GetAudioContentCommentListItem
let isReplyComment: Bool let isReplyComment: Bool
let isShowPopupMenuButton: Bool
let modifyComment: (Int, String) -> Void
let onClickDelete: (Int) -> Void
@State var isShowPopupMenu: Bool = false
@State var isModeModify: Bool = false
@State var comment: String = ""
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 0) { ZStack(alignment: .topTrailing) {
HStack(spacing: 6.7) { VStack(alignment: .leading, spacing: 0) {
KFImage(URL(string: comment.profileUrl)) HStack(spacing: 6.7) {
.resizable() KFImage(URL(string: commentItem.profileUrl))
.frame(width: 40, height: 40)
.clipShape(Circle())
VStack(alignment: .leading, spacing: 0) {
Text(comment.nickname)
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
Text(comment.date)
.font(.custom(Font.medium.rawValue, size: 10.3))
.foregroundColor(Color(hex: "525252"))
.padding(.top, 4)
}
Spacer()
}
if comment.donationCan > 0 {
HStack(spacing: 3) {
Image("ic_can")
.resizable() .resizable()
.frame(width: 13.3, height: 13.3) .frame(width: 40, height: 40)
.clipShape(Circle())
Text("\(comment.donationCan)") VStack(alignment: .leading, spacing: 0) {
.font(.custom(Font.bold.rawValue, size: 12)) Text(commentItem.nickname)
.foregroundColor(.white)
}
.padding(.horizontal, 6.7)
.padding(.vertical, 2.7)
.background(
comment.donationCan >= 100000 ? Color(hex: "973a3a") :
comment.donationCan >= 50000 ? Color(hex: "d85e37") :
comment.donationCan >= 10000 ? Color(hex: "d38c38") :
comment.donationCan >= 5000 ? Color(hex: "59548f") :
comment.donationCan >= 1000 ? Color(hex: "4d6aa4") :
comment.donationCan >= 500 ? Color(hex: "2d7390") :
Color(hex: "548f7d")
)
.cornerRadius(10.7)
.padding(.leading, 46.7)
.padding(.bottom, 5)
}
HStack(spacing: 0) {
VStack(alignment: .leading, spacing: 13.3) {
Text(comment.comment)
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "777777"))
.fixedSize(horizontal: false, vertical: true)
.padding(.top, comment.donationCan > 0 ? 0 : 13.3)
if !isReplyComment {
Text(comment.replyCount > 0 ? "답글 \(comment.replyCount)" : "답글 쓰기")
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "9970ff")) .foregroundColor(Color(hex: "eeeeee"))
Text(commentItem.date)
.font(.custom(Font.medium.rawValue, size: 10.3))
.foregroundColor(Color(hex: "525252"))
.padding(.top, 4)
}
Spacer()
if isShowPopupMenuButton && (contentCreatorId == UserDefaults.int(forKey: .userId) || commentItem.writerId == UserDefaults.int(forKey: .userId)) {
Image("ic_seemore_vertical")
.onTapGesture { isShowPopupMenu.toggle() }
} }
} }
Spacer() if commentItem.donationCan > 0 {
} HStack(spacing: 3) {
.padding(.leading, 46.7) Image("ic_can")
.resizable()
.frame(width: 13.3, height: 13.3)
Rectangle() Text("\(commentItem.donationCan)")
.foregroundColor(Color(hex: "595959")) .font(.custom(Font.bold.rawValue, size: 12))
.frame(height: 0.5) .foregroundColor(.white)
.padding(.top, 16.7) }
.padding(.horizontal, 6.7)
.padding(.vertical, 2.7)
.background(
commentItem.donationCan >= 100000 ? Color(hex: "973a3a") :
commentItem.donationCan >= 50000 ? Color(hex: "d85e37") :
commentItem.donationCan >= 10000 ? Color(hex: "d38c38") :
commentItem.donationCan >= 5000 ? Color(hex: "59548f") :
commentItem.donationCan >= 1000 ? Color(hex: "4d6aa4") :
commentItem.donationCan >= 500 ? Color(hex: "2d7390") :
Color(hex: "548f7d")
)
.cornerRadius(10.7)
.padding(.leading, 46.7)
.padding(.bottom, 5)
}
HStack(spacing: 0) {
if isModeModify {
HStack(spacing: 0) {
TextField("댓글을 입력해 보세요.", text: $comment)
.autocapitalization(.none)
.disableAutocorrection(true)
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
.accentColor(Color(hex: "3bb9f1"))
.keyboardType(.default)
.padding(.horizontal, 13.3)
Spacer()
Image("btn_message_send")
.resizable()
.frame(width: 35, height: 35)
.padding(6.7)
.onTapGesture {
hideKeyboard()
if commentItem.comment != comment {
modifyComment(commentItem.id, comment)
}
isModeModify = false
}
}
.background(Color(hex: "232323"))
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.strokeBorder(lineWidth: 1)
.foregroundColor(Color(hex: "3bb9f1"))
)
} else {
VStack(alignment: .leading, spacing: 13.3) {
Text(commentItem.comment)
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "777777"))
.fixedSize(horizontal: false, vertical: true)
.padding(.top, commentItem.donationCan > 0 ? 0 : 13.3)
if !isReplyComment {
NavigationLink(
destination: AudioContentListReplyView(
creatorId: contentCreatorId,
audioContentId: audioContentId,
parentComment: commentItem
)
) {
Text(commentItem.replyCount > 0 ? "답글 \(commentItem.replyCount)" : "답글 쓰기")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "9970ff"))
}
}
}
}
Spacer()
}
.padding(.leading, 46.7)
Rectangle()
.foregroundColor(Color(hex: "595959"))
.frame(height: 0.5)
.padding(.top, 16.7)
}
if isShowPopupMenu {
VStack(spacing: 10) {
if commentItem.writerId == UserDefaults.int(forKey: .userId) {
Text("수정")
.font(.custom(Font.medium.rawValue, size: 14))
.foregroundColor(Color(hex: "777777"))
.onTapGesture {
isModeModify = true
isShowPopupMenu = false
}
}
if contentCreatorId == UserDefaults.int(forKey: .userId) ||
commentItem.writerId == UserDefaults.int(forKey: .userId)
{
Text("삭제")
.font(.custom(Font.medium.rawValue, size: 14))
.foregroundColor(Color(hex: "777777"))
.onTapGesture {
onClickDelete(commentItem.id)
isShowPopupMenu = false
}
}
}
.padding(10)
.background(Color(hex: "222222"))
}
} }
.onAppear { comment = commentItem.comment }
} }
} }

View File

@ -11,10 +11,15 @@ import Kingfisher
struct AudioContentCommentListView: View { struct AudioContentCommentListView: View {
@Binding var isPresented: Bool @Binding var isPresented: Bool
let creatorId: Int
let audioContentId: Int let audioContentId: Int
@StateObject var viewModel = AudioContentCommentListViewModel() @StateObject var viewModel = AudioContentCommentListViewModel()
@State private var commentId: Int = 0
@State private var isShowDeletePopup: Bool = false
var body: some View { var body: some View {
NavigationView { NavigationView {
ZStack { ZStack {
@ -59,7 +64,7 @@ struct AudioContentCommentListView: View {
.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(hex: "eeeeee"))
.accentColor(Color(hex: "9970ff")) .accentColor(Color(hex: "3bb9f1"))
.keyboardType(.default) .keyboardType(.default)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
@ -79,7 +84,7 @@ struct AudioContentCommentListView: View {
.overlay( .overlay(
RoundedRectangle(cornerRadius: 10) RoundedRectangle(cornerRadius: 10)
.strokeBorder(lineWidth: 1) .strokeBorder(lineWidth: 1)
.foregroundColor(Color(hex: "9970ff")) .foregroundColor(Color(hex: "3bb9f1"))
) )
Spacer() Spacer()
@ -97,13 +102,22 @@ struct AudioContentCommentListView: View {
LazyVStack(spacing: 13.3) { LazyVStack(spacing: 13.3) {
ForEach(0..<viewModel.commentList.count, id: \.self) { index in ForEach(0..<viewModel.commentList.count, id: \.self) { index in
let comment = viewModel.commentList[index] let comment = viewModel.commentList[index]
NavigationLink { VStack {
AudioContentListReplyView( AudioContentCommentItemView(
contentCreatorId: creatorId,
audioContentId: audioContentId, audioContentId: audioContentId,
parentComment: comment commentItem: comment,
isReplyComment: false,
isShowPopupMenuButton: true,
modifyComment: { commentId, comment in
hideKeyboard()
viewModel.modifyComment(commentId: commentId, audioContentId: audioContentId, comment: comment)
},
onClickDelete: {
commentId = $0
isShowDeletePopup = true
}
) )
} label: {
AudioContentCommentItemView(comment: comment, isReplyComment: false)
.padding(.horizontal, 26.7) .padding(.horizontal, 26.7)
.onAppear { .onAppear {
if index == viewModel.commentList.count - 1 { if index == viewModel.commentList.count - 1 {
@ -116,6 +130,24 @@ struct AudioContentCommentListView: View {
} }
} }
if isShowDeletePopup && commentId > 0 {
SodaDialog(
title: "댓글 삭제",
desc: "삭제하시겠습니까?",
confirmButtonTitle: "삭제",
confirmButtonAction: {
viewModel.modifyComment(commentId: commentId, audioContentId: audioContentId, isActive: false)
commentId = 0
isShowDeletePopup = false
},
cancelButtonTitle: "취소",
cancelButtonAction: {
commentId = 0
isShowDeletePopup = false
}
)
}
if viewModel.isLoading { if viewModel.isLoading {
LoadingView() LoadingView()
} }

View File

@ -121,4 +121,77 @@ class AudioContentCommentListViewModel: ObservableObject {
} }
.store(in: &subscription) .store(in: &subscription)
} }
func modifyComment(
commentId: Int,
audioContentId: Int,
comment: String? = nil,
isActive: Bool? = nil
) {
if comment == nil && isActive == nil {
errorMessage = "변경사항이 없습니다."
isShowPopup = true
return
}
if let comment = comment, comment.trimmingCharacters(in: .whitespaces).isEmpty {
errorMessage = "내용을 입력하세요."
isShowPopup = true
return
}
isLoading = true
var request = ModifyCommentRequest(commentId: commentId)
if let comment = comment {
request.comment = comment
}
if let isActive = isActive {
request.isActive = isActive
}
repository.modifyComment(request: request)
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { [unowned self] response in
self.isLoading = false
let responseData = response.data
do {
let jsonDecoder = JSONDecoder()
let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
if decoded.success {
self.comment = ""
self.page = 1
self.isLast = false
self.commentList.removeAll()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.getCommentList()
}
} else {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
}
self.isShowPopup = true
}
} catch {
self.isLoading = false
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
}
}
.store(in: &subscription)
}
} }

View File

@ -10,12 +10,16 @@ import Kingfisher
struct AudioContentListReplyView: View { struct AudioContentListReplyView: View {
let creatorId: Int
let audioContentId: Int let audioContentId: Int
let parentComment: GetAudioContentCommentListItem let parentComment: GetAudioContentCommentListItem
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode> @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@StateObject var viewModel = AudioContentListReplyViewModel() @StateObject var viewModel = AudioContentListReplyViewModel()
@State private var commentId: Int = 0
@State private var isShowDeletePopup: Bool = false
var body: some View { var body: some View {
ZStack { ZStack {
VStack(spacing: 0) { VStack(spacing: 0) {
@ -53,7 +57,7 @@ struct AudioContentListReplyView: View {
.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(hex: "eeeeee"))
.accentColor(Color(hex: "9970ff")) .accentColor(Color(hex: "3bb9f1"))
.keyboardType(.default) .keyboardType(.default)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
@ -73,7 +77,7 @@ struct AudioContentListReplyView: View {
.overlay( .overlay(
RoundedRectangle(cornerRadius: 10) RoundedRectangle(cornerRadius: 10)
.strokeBorder(lineWidth: 1) .strokeBorder(lineWidth: 1)
.foregroundColor(Color(hex: "9970ff")) .foregroundColor(Color(hex: "3bb9f1"))
) )
Spacer() Spacer()
@ -87,7 +91,15 @@ struct AudioContentListReplyView: View {
.padding(.bottom, 13.3) .padding(.bottom, 13.3)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
AudioContentCommentItemView(comment: parentComment, isReplyComment: true) AudioContentCommentItemView(
contentCreatorId: creatorId,
audioContentId: audioContentId,
commentItem: parentComment,
isReplyComment: true,
isShowPopupMenuButton: false,
modifyComment: { _, _ in },
onClickDelete: { _ in }
)
.padding(.horizontal, 26.7) .padding(.horizontal, 26.7)
.padding(.bottom, 13.3) .padding(.bottom, 13.3)
@ -95,7 +107,21 @@ struct AudioContentListReplyView: View {
LazyVStack(spacing: 13.3) { LazyVStack(spacing: 13.3) {
ForEach(0..<viewModel.commentList.count, id: \.self) { index in ForEach(0..<viewModel.commentList.count, id: \.self) { index in
let comment = viewModel.commentList[index] let comment = viewModel.commentList[index]
AudioContentCommentItemView(comment: comment, isReplyComment: true) AudioContentCommentItemView(
contentCreatorId: creatorId,
audioContentId: audioContentId,
commentItem: comment,
isReplyComment: true,
isShowPopupMenuButton: true,
modifyComment: { commentId, comment in
hideKeyboard()
viewModel.modifyComment(commentId: commentId, audioContentId: audioContentId, comment: comment)
},
onClickDelete: {
commentId = $0
isShowDeletePopup = true
}
)
.padding(.horizontal, 40) .padding(.horizontal, 40)
.onAppear { .onAppear {
if index == viewModel.commentList.count - 1 { if index == viewModel.commentList.count - 1 {
@ -108,6 +134,28 @@ struct AudioContentListReplyView: View {
} }
.navigationTitle("") .navigationTitle("")
.navigationBarBackButtonHidden() .navigationBarBackButtonHidden()
if isShowDeletePopup && commentId > 0 {
SodaDialog(
title: "댓글 삭제",
desc: "삭제하시겠습니까?",
confirmButtonTitle: "삭제",
confirmButtonAction: {
viewModel.modifyComment(commentId: commentId, audioContentId: audioContentId, isActive: false)
commentId = 0
isShowDeletePopup = false
},
cancelButtonTitle: "취소",
cancelButtonAction: {
commentId = 0
isShowDeletePopup = false
}
)
}
if viewModel.isLoading {
LoadingView()
}
} }
.onAppear { .onAppear {
viewModel.audioContentId = audioContentId viewModel.audioContentId = audioContentId

View File

@ -121,4 +121,77 @@ final class AudioContentListReplyViewModel: ObservableObject {
} }
.store(in: &subscription) .store(in: &subscription)
} }
func modifyComment(
commentId: Int,
audioContentId: Int,
comment: String? = nil,
isActive: Bool? = nil
) {
if comment == nil && isActive == nil {
errorMessage = "변경사항이 없습니다."
isShowPopup = true
return
}
if let comment = comment, comment.trimmingCharacters(in: .whitespaces).isEmpty {
errorMessage = "내용을 입력하세요."
isShowPopup = true
return
}
isLoading = true
var request = ModifyCommentRequest(commentId: commentId)
if let comment = comment {
request.comment = comment
}
if let isActive = isActive {
request.isActive = isActive
}
repository.modifyComment(request: request)
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { [unowned self] response in
self.isLoading = false
let responseData = response.data
do {
let jsonDecoder = JSONDecoder()
let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
if decoded.success {
self.comment = ""
self.page = 1
self.isLast = false
self.commentList.removeAll()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.getCommentList()
}
} else {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
}
self.isShowPopup = true
}
} catch {
self.isLoading = false
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
}
}
.store(in: &subscription)
}
} }

View File

@ -58,7 +58,7 @@ struct ContentDetailCommentView: View {
.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(hex: "eeeeee"))
.accentColor(Color(hex: "9970ff")) .accentColor(Color(hex: "3bb9f1"))
.keyboardType(.default) .keyboardType(.default)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
@ -78,7 +78,7 @@ struct ContentDetailCommentView: View {
.overlay( .overlay(
RoundedRectangle(cornerRadius: 10) RoundedRectangle(cornerRadius: 10)
.strokeBorder(lineWidth: 1) .strokeBorder(lineWidth: 1)
.foregroundColor(Color(hex: "9970ff")) .foregroundColor(Color(hex: "3bb9f1"))
) )
} }

View File

@ -0,0 +1,12 @@
//
// ModifyCommentRequest.swift
// SodaLive
//
// Created by klaus on 2023/09/08.
//
struct ModifyCommentRequest: Encodable {
let commentId: Int
var comment: String? = nil
var isActive: Bool? = nil
}

View File

@ -43,7 +43,7 @@ struct ContentDetailOtherContentView: View {
} }
.padding(13.3) .padding(13.3)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.background(Color(hex: "2b2635")) .background(Color(hex: "13181b"))
.cornerRadius(4.7) .cornerRadius(4.7)
} }
} }

View File

@ -12,6 +12,7 @@ import Sliders
struct ContentDetailPlayView: View { struct ContentDetailPlayView: View {
let audioContent: GetAudioContentDetailResponse let audioContent: GetAudioContentDetailResponse
let isAlertPreview: Bool
@Binding var isShowPreviewAlert: Bool @Binding var isShowPreviewAlert: Bool
@StateObject var contentPlayManager = ContentPlayManager.shared @StateObject var contentPlayManager = ContentPlayManager.shared
@ -34,7 +35,7 @@ struct ContentDetailPlayView: View {
) )
.cornerRadius(10.7, corners: [.topLeft, .topRight]) .cornerRadius(10.7, corners: [.topLeft, .topRight])
Image(isPlaying() ? "btn_audio_content_pause" : "btn_audio_content_play") Image(isPlaying() ? "btn_audio_content_pause" : isAlertPreview ? "btn_audio_content_preview_play" : "btn_audio_content_play")
.onTapGesture { .onTapGesture {
if isPlaying() { if isPlaying() {
contentPlayManager.pauseAudio() contentPlayManager.pauseAudio()

View File

@ -66,61 +66,57 @@ struct ContentDetailView: View {
viewModel.getAudioContentDetail() viewModel.getAudioContentDetail()
}) { }) {
VStack(spacing: 0) { VStack(spacing: 0) {
LazyVStack(spacing: 0) { ContentDetailPlayView(
ContentDetailPlayView( audioContent: audioContent,
audioContent: audioContent, isAlertPreview: audioContent.price > 0 && !audioContent.existOrdered && audioContent.orderType == nil && audioContent.creator.creatorId != UserDefaults.int(forKey: .userId),
isShowPreviewAlert: $viewModel.isShowPreviewAlert isShowPreviewAlert: $viewModel.isShowPreviewAlert
) )
ContentDetailInfoView( ContentDetailInfoView(
isExpandDescription: $viewModel.isExpandDescription, isExpandDescription: $viewModel.isExpandDescription,
isShowPreviewAlert: $viewModel.isShowPreviewAlert, isShowPreviewAlert: $viewModel.isShowPreviewAlert,
audioContent: audioContent, audioContent: audioContent,
onClickLike: { viewModel.likeContent() }, onClickLike: { viewModel.likeContent() },
onClickShare: { onClickShare: {
viewModel.shareAudioContent( viewModel.shareAudioContent(
contentImage: audioContent.coverImageUrl, contentImage: audioContent.coverImageUrl,
contentTitle: "\(audioContent.title) - \(audioContent.creator.nickname)" contentTitle: "\(audioContent.title) - \(audioContent.creator.nickname)"
)
},
onClickDonation: { viewModel.isShowDonationPopup = true }
)
if audioContent.price > 0 &&
!audioContent.existOrdered &&
audioContent.orderType == nil &&
audioContent.creator.creatorId != UserDefaults.int(forKey: .userId) {
ContentDetailPurchaseButton(price: audioContent.price)
.contentShape(Rectangle())
.onTapGesture { isShowOrderView = true }
}
if audioContent.isCommentAvailable {
ContentDetailCommentView(
commentCount: audioContent.commentCount,
commentList: audioContent.commentList,
registerComment: { comment in
self.viewModel.registerComment(comment: comment)
}
) )
.padding(10.3) },
.background(Color.white.opacity(0.1)) onClickDonation: { viewModel.isShowDonationPopup = true }
.cornerRadius(5.3) )
.padding(.top, 13.3) .padding(.horizontal, 13.3)
if audioContent.price > 0 &&
!audioContent.existOrdered &&
audioContent.orderType == nil &&
audioContent.creator.creatorId != UserDefaults.int(forKey: .userId) {
ContentDetailPurchaseButton(price: audioContent.price)
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { .padding(.horizontal, 13.3)
if audioContent.commentCount > 0 { .onTapGesture { isShowOrderView = true }
isShowCommentListView = true }
}
if audioContent.isCommentAvailable {
ContentDetailCommentView(
commentCount: audioContent.commentCount,
commentList: audioContent.commentList,
registerComment: { comment in
self.viewModel.registerComment(comment: comment)
}
)
.padding(10.3)
.background(Color.white.opacity(0.1))
.cornerRadius(5.3)
.padding(.top, 13.3)
.contentShape(Rectangle())
.padding(.horizontal, 13.3)
.onTapGesture {
if audioContent.commentCount > 0 {
isShowCommentListView = true
} }
} }
} }
.padding(.horizontal, 13.3)
Rectangle()
.foregroundColor(Color(hex: "232323"))
.frame(height: 6.7)
.padding(.top, 24)
ContentDetailOtherContentView( ContentDetailOtherContentView(
title: "크리에이터의 다른 콘텐츠", title: "크리에이터의 다른 콘텐츠",
@ -282,6 +278,7 @@ struct ContentDetailView: View {
content: { content: {
AudioContentCommentListView( AudioContentCommentListView(
isPresented: $isShowCommentListView, isPresented: $isShowCommentListView,
creatorId: viewModel.audioContent!.creator.creatorId,
audioContentId: viewModel.audioContent!.contentId audioContentId: viewModel.audioContent!.contentId
) )
} }

View File

@ -90,7 +90,7 @@ struct ContentOrderConfirmDialogView: View {
.resizable() .resizable()
.frame(width: 16.7, height: 16.7) .frame(width: 16.7, height: 16.7)
Text("\(orderType == .RENTAL ? Int(ceil(Double(audioContent.price) * 0.7)) : audioContent.price)") Text("\(orderType == .RENTAL ? Int(ceil(Double(audioContent.price) * 0.6)) : audioContent.price)")
.font(.custom(Font.bold.rawValue, size: 13.3)) .font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))

View File

@ -43,7 +43,7 @@ struct ContentOrderDialogView: View {
.resizable() .resizable()
.frame(width: 16.7, height: 16.7) .frame(width: 16.7, height: 16.7)
Text("\(Int(ceil(Double(price) * 0.7)))") Text("\(Int(ceil(Double(price) * 0.6)))")
.font(.custom(Font.bold.rawValue, size: 13.3)) .font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
} }

View File

@ -12,11 +12,12 @@ import Kingfisher
struct LiveRoomDonationDialogView: View { struct LiveRoomDonationDialogView: View {
@AppStorage("can") private var can: Int = UserDefaults.int(forKey: .can)
@State private var donationCan = "" @State private var donationCan = ""
@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 can = 0
@Binding var isShowing: Bool @Binding var isShowing: Bool
let isAudioContentDonation: Bool let isAudioContentDonation: Bool
@ -243,9 +244,6 @@ struct LiveRoomDonationDialogView: View {
} }
.offset(y: isAudioContentDonation ? 0 : 0 - keyboardHandler.keyboardHeight) .offset(y: isAudioContentDonation ? 0 : 0 - keyboardHandler.keyboardHeight)
} }
.onAppear {
self.can = UserDefaults.int(forKey: .can)
}
} }
func limitText() { func limitText() {

View File

@ -13,9 +13,26 @@ struct ContentMainCurationItemView: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
Text(item.title) HStack(spacing: 0) {
.font(.custom(Font.bold.rawValue, size: 18.3)) Text(item.title)
.foregroundColor(Color(hex: "eeeeee")) .font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "eeeeee"))
Spacer()
Image("ic_forward")
.resizable()
.frame(width: 20, height: 20)
.onTapGesture {
AppState.shared
.setAppStep(
step: .curationAll(
title: item.title,
curationId: item.curationId
)
)
}
}
Text(item.description) Text(item.description)
.font(.custom(Font.medium.rawValue, size: 13)) .font(.custom(Font.medium.rawValue, size: 13))

View File

@ -14,8 +14,8 @@ struct ContentMainCurationView: View {
var body: some View { var body: some View {
LazyVStack(spacing: 40) { LazyVStack(spacing: 40) {
ForEach(0..<items.count, id: \.self) { ForEach(0..<items.count, id: \.self) {
let item = items[$0] ContentMainCurationItemView(item: items[$0])
ContentMainCurationItemView(item: item) .padding(.horizontal, 13.3)
} }
} }
} }

View File

@ -23,7 +23,9 @@ struct ContentMainMyStashView: View {
Text("전체보기") Text("전체보기")
.font(.custom(Font.light.rawValue, size: 11.3)) .font(.custom(Font.light.rawValue, size: 11.3))
.foregroundColor(Color(hex: "bbbbbb")) .foregroundColor(Color(hex: "bbbbbb"))
.onTapGesture {} .onTapGesture {
AppState.shared.setAppStep(step: .orderListAll)
}
} }
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {

View File

@ -33,7 +33,11 @@ struct ContentMainNewContentThemeView: View {
.stroke(lineWidth: 0.5) .stroke(lineWidth: 0.5)
.foregroundColor(Color(hex: selectedTheme == theme ? "9970ff" : "eeeeee")) .foregroundColor(Color(hex: selectedTheme == theme ? "9970ff" : "eeeeee"))
) )
.onTapGesture { selectTheme(theme) } .onTapGesture {
if selectedTheme != theme {
selectTheme(theme)
}
}
} }
} }
} }

View File

@ -17,9 +17,20 @@ struct ContentMainNewContentView: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
Text("새로운 콘텐츠") HStack(spacing: 0) {
.font(.custom(Font.bold.rawValue, size: 18.3)) Text("새로운 콘텐츠")
.foregroundColor(Color(hex: "eeeeee")) .font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "eeeeee"))
Spacer()
Image("ic_forward")
.resizable()
.frame(width: 20, height: 20)
.onTapGesture {
AppState.shared.setAppStep(step: .newContentAll)
}
}
ContentMainNewContentThemeView(themes: themes, selectTheme: selectTheme, selectedTheme: $selectedTheme) ContentMainNewContentThemeView(themes: themes, selectTheme: selectTheme, selectedTheme: $selectedTheme)
.padding(.vertical, 16.7) .padding(.vertical, 16.7)

View File

@ -0,0 +1,143 @@
//
// ContentMainRankingView.swift
// SodaLive
//
// Created by klaus on 2023/10/15.
//
import SwiftUI
import Kingfisher
struct ContentMainRankingView: View {
let item: GetAudioContentRanking
let rows = [
GridItem(.fixed(60), alignment: .leading),
GridItem(.fixed(60), alignment: .leading),
GridItem(.fixed(60), alignment: .leading)
]
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 0) {
Text("인기 콘텐츠")
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "eeeeee"))
Spacer()
Image("ic_forward")
.onTapGesture {
AppState.shared.setAppStep(step: .contentRankingAll)
}
}
VStack(spacing: 8) {
Text("\(item.startDate) ~ \(item.endDate)")
.font(.custom(Font.bold.rawValue, size: 14.7))
.foregroundColor(Color(hex: "eeeeee"))
Text("※ 인기 콘텐츠의 순위는 매주 업데이트됩니다.")
.font(.custom(Font.light.rawValue, size: 13.3))
.foregroundColor(Color(hex: "bbbbbb"))
}
.padding(.vertical, 8)
.frame(width: screenSize().width - 26.7)
.background(Color(hex: "222222"))
.padding(.top, 13.3)
ScrollView(.horizontal, showsIndicators: false) {
LazyHGrid(rows: rows, spacing: 13.3) {
ForEach(0..<item.items.count, id: \.self) { index in
let content = item.items[index]
HStack(spacing: 0) {
KFImage(URL(string: content.coverImageUrl))
.resizable()
.frame(width: 60, height: 60)
.cornerRadius(2.7)
Text("\(index + 1)")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "3bb9f1"))
.padding(.horizontal, 12)
VStack(alignment: .leading, spacing: 8) {
Text(content.title)
.lineLimit(2)
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "d2d2d2"))
Text(content.creatorNickname)
.font(.custom(Font.medium.rawValue, size: 11))
.foregroundColor(Color(hex: "777777"))
}
}
.frame(maxWidth: screenSize().width * 0.66, alignment: .leading)
.contentShape(Rectangle())
.onTapGesture {
AppState
.shared
.setAppStep(step: .contentDetail(contentId: content.contentId))
}
}
}
.frame(height: 207)
}
.padding(.top, 13.3)
}
}
}
struct ContentMainRankingView_Previews: PreviewProvider {
static var previews: some View {
ContentMainRankingView(
item: GetAudioContentRanking(
startDate: "2023년 10월 2일",
endDate: "10월 9일",
items: [
GetAudioContentRankingItem(
contentId: 10,
title: "sdffsfddfs",
coverImageUrl: "https://test-cf.sodalive.net/audio_content_cover/27/27-cover-77e3a23f-23f2-467c-867f-3e6b7e08060c-9136-1696424061456",
themeStr: "커버곡",
price: 10,
duration: "00:30:20",
creatorId: 1,
creatorNickname: "ㄹㄴ어ㅏㅣㅇㄴ런아ㅣ"
),
GetAudioContentRankingItem(
contentId: 10,
title: "sdffsfddfs",
coverImageUrl: "https://test-cf.sodalive.net/audio_content_cover/27/27-cover-77e3a23f-23f2-467c-867f-3e6b7e08060c-9136-1696424061456",
themeStr: "커버곡",
price: 10,
duration: "00:30:20",
creatorId: 1,
creatorNickname: "ㄹㄴ어ㅏㅣㅇㄴ런아ㅣ"
),
GetAudioContentRankingItem(
contentId: 10,
title: "sdffsfddfs",
coverImageUrl: "https://test-cf.sodalive.net/audio_content_cover/27/27-cover-77e3a23f-23f2-467c-867f-3e6b7e08060c-9136-1696424061456",
themeStr: "커버곡",
price: 10,
duration: "00:30:20",
creatorId: 1,
creatorNickname: "ㄹㄴ어ㅏㅣㅇㄴ런아ㅣ"
),
GetAudioContentRankingItem(
contentId: 10,
title: "sdffsfddfs",
coverImageUrl: "https://test-cf.sodalive.net/audio_content_cover/27/27-cover-77e3a23f-23f2-467c-867f-3e6b7e08060c-9136-1696424061456",
themeStr: "커버곡",
price: 10,
duration: "00:30:20",
creatorId: 1,
creatorNickname: "ㄹㄴ어ㅏㅣㅇㄴ런아ㅣ"
),
]
)
)
}
}

View File

@ -27,20 +27,24 @@ struct ContentMainView: View {
.font(.custom(Font.bold.rawValue, size: 21.3)) .font(.custom(Font.bold.rawValue, size: 21.3))
.foregroundColor(Color(hex: "9970ff")) .foregroundColor(Color(hex: "9970ff"))
.padding(.bottom, 26.7) .padding(.bottom, 26.7)
.padding(.horizontal, 13.3)
if viewModel.newContentUploadCreatorList.count > 0 { if viewModel.newContentUploadCreatorList.count > 0 {
ContentMainNewContentCreatorView(items: viewModel.newContentUploadCreatorList) ContentMainNewContentCreatorView(items: viewModel.newContentUploadCreatorList)
.padding(.bottom, 26.7) .padding(.bottom, 26.7)
.padding(.horizontal, 13.3)
} }
if viewModel.bannerList.count > 0 { if viewModel.bannerList.count > 0 {
ContentMainBannerView(items: viewModel.bannerList) ContentMainBannerView(items: viewModel.bannerList)
.padding(.bottom, 40) .padding(.bottom, 40)
.padding(.horizontal, 13.3)
} }
if viewModel.orderList.count > 0 { if viewModel.orderList.count > 0 {
ContentMainMyStashView(items: viewModel.orderList) ContentMainMyStashView(items: viewModel.orderList)
.padding(.bottom, 40) .padding(.bottom, 40)
.padding(.horizontal, 13.3)
} }
ContentMainNewContentView( ContentMainNewContentView(
@ -49,6 +53,13 @@ struct ContentMainView: View {
selectTheme: { viewModel.selectedTheme = $0 }, selectTheme: { viewModel.selectedTheme = $0 },
selectedTheme: $viewModel.selectedTheme selectedTheme: $viewModel.selectedTheme
) )
.padding(.horizontal, 13.3)
if let contentRanking = viewModel.contentRanking {
ContentMainRankingView(item: contentRanking)
.padding(.top, 40)
.padding(.horizontal, 13.3)
}
if viewModel.curationList.count > 0 { if viewModel.curationList.count > 0 {
ContentMainCurationView(items: viewModel.curationList) ContentMainCurationView(items: viewModel.curationList)
@ -56,7 +67,7 @@ struct ContentMainView: View {
.padding(.bottom, 20) .padding(.bottom, 20)
} }
} }
.padding(13.3) .padding(.vertical, 13.3)
} }
if UserDefaults.string(forKey: .role) == MemberRole.CREATOR.rawValue { if UserDefaults.string(forKey: .role) == MemberRole.CREATOR.rawValue {

View File

@ -24,6 +24,7 @@ final class ContentMainViewModel: ObservableObject {
@Published var orderList = [GetAudioContentMainItem]() @Published var orderList = [GetAudioContentMainItem]()
@Published var themeList = [String]() @Published var themeList = [String]()
@Published var curationList = [GetAudioContentCurationResponse]() @Published var curationList = [GetAudioContentCurationResponse]()
@Published var contentRanking: GetAudioContentRanking? = nil
@Published var selectedTheme = "전체" { @Published var selectedTheme = "전체" {
didSet { didSet {
@ -64,6 +65,7 @@ final class ContentMainViewModel: ObservableObject {
self.bannerList.append(contentsOf: data.bannerList) self.bannerList.append(contentsOf: data.bannerList)
self.orderList.append(contentsOf: data.orderList) self.orderList.append(contentsOf: data.orderList)
self.curationList.append(contentsOf: data.curationList) self.curationList.append(contentsOf: data.curationList)
self.contentRanking = data.contentRanking
self.themeList.append("전체") self.themeList.append("전체")
self.themeList.append(contentsOf: data.themeList) self.themeList.append(contentsOf: data.themeList)

View File

@ -14,6 +14,24 @@ struct GetAudioContentMainResponse: Decodable {
let themeList: [String] let themeList: [String]
let newContentList: [GetAudioContentMainItem] let newContentList: [GetAudioContentMainItem]
let curationList: [GetAudioContentCurationResponse] let curationList: [GetAudioContentCurationResponse]
let contentRanking: GetAudioContentRanking
}
struct GetAudioContentRanking: Decodable {
let startDate: String
let endDate: String
let items: [GetAudioContentRankingItem]
}
struct GetAudioContentRankingItem: Decodable {
let contentId: Int
let title: String
let coverImageUrl: String
let themeStr: String
let price: Int
let duration: String
let creatorId: Int
let creatorNickname: String
} }
struct GetNewContentUploadCreator: Decodable { struct GetNewContentUploadCreator: Decodable {
@ -33,6 +51,7 @@ struct GetAudioContentMainItem: Decodable {
} }
struct GetAudioContentCurationResponse: Decodable { struct GetAudioContentCurationResponse: Decodable {
let curationId: Int
let title: String let title: String
let description: String let description: String
let contents: [GetAudioContentMainItem] let contents: [GetAudioContentMainItem]

View File

@ -151,6 +151,21 @@ struct ContentView: View {
case .orderListAll: case .orderListAll:
OrderListAllView() OrderListAllView()
case .userProfileDonationAll(let userId):
UserProfileDonationAllView(userId: userId)
case .userProfileFanTalkAll(let userId):
UserProfileFanTalkAllView(userId: userId)
case .newContentAll:
ContentNewAllView()
case .curationAll(let title, let curationId):
ContentCurationView(title: title, curationId: curationId)
case .contentRankingAll:
ContentRankingAllView()
default: default:
EmptyView() EmptyView()
.frame(width: 0, height: 0, alignment: .topLeading) .frame(width: 0, height: 0, alignment: .topLeading)

View File

@ -15,8 +15,9 @@ enum ExplorerApi {
case getFollowerList(userId: Int, page: Int, size: Int) case getFollowerList(userId: Int, page: Int, size: Int)
case getCreatorProfileCheers(userId: Int, page: Int, size: Int) case getCreatorProfileCheers(userId: Int, page: Int, size: Int)
case writeCheers(parentCheersId: Int?, creatorId: Int, content: String) case writeCheers(parentCheersId: Int?, creatorId: Int, content: String)
case modifyCheers(cheersId: Int, content: String) case modifyCheers(request: PutModifyCheersRequest)
case writeCreatorNotice(request: PostCreatorNoticeRequest) case writeCreatorNotice(request: PostCreatorNoticeRequest)
case getCreatorProfileDonationRanking(userId: Int, page: Int, size: Int)
} }
extension ExplorerApi: TargetType { extension ExplorerApi: TargetType {
@ -35,6 +36,9 @@ extension ExplorerApi: TargetType {
case .getCreatorProfile(let userId): case .getCreatorProfile(let userId):
return "/explorer/profile/\(userId)" return "/explorer/profile/\(userId)"
case .getCreatorProfileDonationRanking(let userId, _, _):
return "/explorer/profile/\(userId)/donation-rank"
case .getFollowerList(let userId, _, _): case .getFollowerList(let userId, _, _):
return "/explorer/profile/\(userId)/follower-list" return "/explorer/profile/\(userId)/follower-list"
@ -54,7 +58,7 @@ extension ExplorerApi: TargetType {
var method: Moya.Method { var method: Moya.Method {
switch self { switch self {
case .getExplorer, .searchChannel, .getCreatorProfile, .getFollowerList, .getCreatorProfileCheers: case .getExplorer, .searchChannel, .getCreatorProfile, .getFollowerList, .getCreatorProfileCheers, .getCreatorProfileDonationRanking:
return .get return .get
case .writeCheers, .writeCreatorNotice: case .writeCheers, .writeCreatorNotice:
@ -98,12 +102,19 @@ extension ExplorerApi: TargetType {
let request = PostWriteCheersRequest(parentId: parentCheersId, creatorId: creatorId, content: content) let request = PostWriteCheersRequest(parentId: parentCheersId, creatorId: creatorId, content: content)
return .requestJSONEncodable(request) return .requestJSONEncodable(request)
case .modifyCheers(let cheersId, let content): case .modifyCheers(let request):
let request = PutModifyCheersRequest(cheersId: cheersId, content: content)
return .requestJSONEncodable(request) return .requestJSONEncodable(request)
case .writeCreatorNotice(let request): case .writeCreatorNotice(let request):
return .requestJSONEncodable(request) return .requestJSONEncodable(request)
case .getCreatorProfileDonationRanking(_, let page, let size):
let parameters = [
"page": page - 1,
"size": size
] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
} }
} }

View File

@ -37,11 +37,16 @@ final class ExplorerRepository {
return api.requestPublisher(.writeCheers(parentCheersId: parentCheersId, creatorId: creatorId, content: content)) return api.requestPublisher(.writeCheers(parentCheersId: parentCheersId, creatorId: creatorId, content: content))
} }
func modifyCheers(cheersId: Int, content: String) -> AnyPublisher<Response, MoyaError> { func modifyCheers(cheersId: Int, content: String?, isActive: Bool?) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.modifyCheers(cheersId: cheersId, content: content)) let request = PutModifyCheersRequest(cheersId: cheersId, content: content, isActive: isActive)
return api.requestPublisher(.modifyCheers(request: request))
} }
func writeCreatorNotice(notice: String) -> AnyPublisher<Response, MoyaError> { func writeCreatorNotice(notice: String) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.writeCreatorNotice(request: PostCreatorNoticeRequest(notice: notice))) return api.requestPublisher(.writeCreatorNotice(request: PostCreatorNoticeRequest(notice: notice)))
} }
func getCreatorProfileDonationRanking(userId: Int, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getCreatorProfileDonationRanking(userId: userId, page: page, size: size))
}
} }

View File

@ -0,0 +1,147 @@
//
// ExplorerSectionView.swift
// SodaLive
//
// Created by klaus on 2023/10/14.
//
import SwiftUI
import Kingfisher
struct ExplorerSectionView: View {
let section: GetExplorerSectionResponse
let rankingCrawns = ["ic_crown_1", "ic_crown_2", "ic_crown_3"]
let rankingColors = [
[Color(hex: "ffdc00"), Color(hex: "ffb600")],
[Color(hex: "ffffff"), Color(hex: "9f9f9f")],
[Color(hex: "e6a77a"), Color(hex: "c67e4a")],
[Color(hex: "ffffff").opacity(0), Color(hex: "ffffff").opacity(0)]
]
var body: some View {
VStack(alignment: .leading, spacing: 13.3) {
VStack(alignment: .leading, spacing: 4) {
if let coloredTitle = section.coloredTitle, let color = section.color {
let titleArray = section.title.components(separatedBy: coloredTitle)
HStack(spacing: 0) {
Text(titleArray[0])
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "eeeeee"))
Text(coloredTitle)
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: color))
if titleArray.count > 1 {
Text(titleArray[1])
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "eeeeee"))
}
}
} else {
Text(section.title)
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "eeeeee"))
}
if let desc = section.desc {
VStack(spacing: 8) {
Text("\(desc)")
.font(.custom(Font.bold.rawValue, size: 14.7))
.foregroundColor(Color(hex: "eeeeee"))
Text("※ 인기 크리에이터의 순위는 매주 업데이트됩니다.")
.font(.custom(Font.light.rawValue, size: 13.3))
.foregroundColor(Color(hex: "bbbbbb"))
}
.padding(.vertical, 8)
.frame(width: screenSize().width - 26.7)
.background(Color(hex: "222222"))
.padding(.top, 13.3)
}
}
.frame(width: screenSize().width - 26.7, alignment: .leading)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 13.3) {
ForEach(0..<section.creators.count, id: \.self) { index in
let creator = section.creators[index]
VStack(spacing: 0) {
if let _ = section.desc {
ZStack {
KFImage(URL(string: creator.profileImageUrl))
.cancelOnDisappear(true)
.downsampling(size: CGSize(width: 90, height: 90))
.resizable()
.clipShape(Circle())
.frame(width: 90, height: 90)
.overlay(
Circle()
.stroke(
AngularGradient(colors: rankingColors[index < 4 ? index : 3], center: .center),
lineWidth: 3
)
)
if index < 3 {
VStack(alignment: .trailing, spacing: 0) {
Spacer()
Image(rankingCrawns[index])
.resizable()
.frame(width: 37, height: 37)
}
.frame(width: 93.3, height: 93.3, alignment: .trailing)
}
}
.frame(width: 93.3, height: 93.3)
} else {
KFImage(URL(string: creator.profileImageUrl))
.cancelOnDisappear(true)
.downsampling(size: CGSize(width: 93, height: 93))
.resizable()
.clipShape(Circle())
.frame(width: 93, height: 93)
}
Text(creator.nickname)
.font(.custom(Font.medium.rawValue, size: 11.3))
.foregroundColor(Color(hex: "eeeeee"))
.lineLimit(1)
.frame(width: 93.3)
.padding(.top, 13.3)
Text(creator.tags)
.font(.custom(Font.medium.rawValue, size: 10))
.foregroundColor(Color(hex: "9970ff"))
.lineLimit(1)
.frame(width: 93.3)
.padding(.top, 3.3)
}
.contentShape(Rectangle())
.onTapGesture {
AppState.shared
.setAppStep(step: .creatorDetail(userId: creator.id))
}
}
}
}
.frame(width: screenSize().width - 26.7, alignment: .leading)
}
}
}
struct ExplorerSectionView_Previews: PreviewProvider {
static var previews: some View {
ExplorerSectionView(
section: GetExplorerSectionResponse(
title: "",
coloredTitle: nil,
color: nil,
desc: nil,
creators: []
)
)
}
}

View File

@ -23,7 +23,7 @@ struct ExplorerView: View {
.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(hex: "eeeeee"))
.accentColor(Color(hex: "9970ff")) .accentColor(Color(hex: "3bb9f1"))
.keyboardType(.default) .keyboardType(.default)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
@ -83,68 +83,8 @@ struct ExplorerView: View {
} else { } else {
ScrollView(.vertical, showsIndicators: false) { ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 26.7) { VStack(spacing: 26.7) {
ForEach(viewModel.explorerSections, id: \.self) { section in ForEach(0..<viewModel.explorerSections.count, id: \.self) { index in
VStack(alignment: .leading, spacing: 13.3) { ExplorerSectionView(section: viewModel.explorerSections[index])
if let coloredTitle = section.coloredTitle, let color = section.color {
let titleArray = section.title.components(separatedBy: coloredTitle)
HStack(spacing: 0) {
Text(titleArray[0])
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "eeeeee"))
Text(coloredTitle)
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: color))
if titleArray.count > 1 {
Text(titleArray[1])
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "eeeeee"))
}
}
.frame(width: screenSize().width - 26.7, alignment: .leading)
} else {
Text(section.title)
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "eeeeee"))
.frame(width: screenSize().width - 26.7, alignment: .leading)
}
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 13.3) {
ForEach(section.creators, id: \.self) { creator in
VStack(spacing: 0) {
KFImage(URL(string: creator.profileImageUrl))
.cancelOnDisappear(true)
.downsampling(size: CGSize(width: 93.3, height: 93.3))
.resizable()
.frame(width: 93.3, height: 93.3)
.clipShape(Circle())
Text(creator.nickname)
.font(.custom(Font.medium.rawValue, size: 11.3))
.foregroundColor(Color(hex: "eeeeee"))
.lineLimit(1)
.frame(width: 93.3)
.padding(.top, 13.3)
Text(creator.tags)
.font(.custom(Font.medium.rawValue, size: 10))
.foregroundColor(Color(hex: "9970ff"))
.lineLimit(1)
.frame(width: 93.3)
.padding(.top, 3.3)
}
.contentShape(Rectangle())
.onTapGesture {
AppState.shared
.setAppStep(step: .creatorDetail(userId: creator.id))
}
}
}
}
.frame(width: screenSize().width - 26.7, alignment: .leading)
}
} }
} }
.padding(.vertical, 40) .padding(.vertical, 40)

View File

@ -15,6 +15,7 @@ struct GetExplorerSectionResponse: Decodable, Hashable {
let title: String let title: String
let coloredTitle: String? let coloredTitle: String?
let color: String? let color: String?
let desc: String?
let creators: [GetExplorerSectionCreatorResponse] let creators: [GetExplorerSectionCreatorResponse]
} }

View File

@ -15,5 +15,6 @@ struct PostWriteCheersRequest: Encodable {
struct PutModifyCheersRequest: Encodable { struct PutModifyCheersRequest: Encodable {
let cheersId: Int let cheersId: Int
let content: String let content: String?
let isActive: Bool?
} }

View File

@ -0,0 +1,178 @@
//
// UserProfileFanTalkAllView.swift
// SodaLive
//
// Created by klaus on 2023/08/29.
//
import SwiftUI
import Kingfisher
struct UserProfileFanTalkAllView: View {
let userId: Int
@StateObject var viewModel = UserProfileFanTalkViewModel()
@State private var cheersContent: String = ""
@State private var cheersId: Int = 0
var body: some View {
GeometryReader { proxy in
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) {
DetailNavigationBar(title: "팬 Talk 전체보기")
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 6.7) {
Text("응원")
.font(.custom(Font.medium.rawValue, size: 14.7))
.foregroundColor(Color(hex: "eeeeee"))
Text("\(viewModel.cheersTotalCount)")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "777777"))
}
.padding(.top, 20)
Rectangle()
.frame(height: 1)
.foregroundColor(Color(hex: "909090").opacity(0.5))
.padding(.top, 13.3)
HStack(spacing: 0) {
TextField("응원댓글을 입력하세요", text: $cheersContent)
.autocapitalization(.none)
.disableAutocorrection(true)
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
.accentColor(Color(hex: "3bb9f1"))
.keyboardType(.default)
.padding(.horizontal, 13.3)
Spacer()
Image("btn_message_send")
.resizable()
.frame(width: 35, height: 35)
.padding(6.7)
.onTapGesture {
hideKeyboard()
viewModel.writeCheers(creatorId: userId, cheersContent: cheersContent)
cheersContent = ""
}
}
.background(Color(hex: "232323"))
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.strokeBorder(lineWidth: 1)
.foregroundColor(Color(hex: "3bb9f1"))
)
.padding(.top, 13.3)
Rectangle()
.frame(height: 1)
.foregroundColor(Color(hex: "909090").opacity(0.5))
.padding(.top, 13.3)
ScrollView(.vertical, showsIndicators: false) {
LazyVStack(spacing: 20) {
if viewModel.cheersTotalCount > 0 {
ForEach(0..<viewModel.cheersList.count, id: \.self) { index in
let cheer = viewModel.cheersList[index]
UserProfileFanTalkCheersItemView(
userId: userId,
cheersItem: cheer,
writeCheerReply: { cheersReplyContent in
viewModel.writeCheersReply(parentCheersId: cheer.cheersId, creatorId: userId, cheersReplyContent: cheersReplyContent)
},
modifyCheer: { cheersId, cheersReplyContent in
viewModel.modifyCheers(cheersId: cheersId, creatorId: userId, cheersContent: cheersReplyContent)
},
reportPopup: { cheersId in
viewModel.reportCheersId = cheersId
viewModel.isShowCheersReportView = true
},
onClickDelete: { cheersId in
self.cheersId = cheersId
viewModel.isShowCheersDeleteView = true
}
)
.onAppear {
if index == viewModel.cheersList.count - 1 {
viewModel.getCheersList(creatorId: userId)
}
}
.onTapGesture {
hideKeyboard()
}
}
} else {
Text("응원이 없습니다.\n\n처음으로 응원을 해보세요!")
.font(.custom(Font.light.rawValue, size: 13.3))
.foregroundColor(Color(hex: "bbbbbb"))
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
.padding(.vertical, 60)
.frame(width: screenSize().width - 26.7)
Spacer()
}
}
}
.padding(.top, 20)
}
.frame(width: screenSize().width - 26.7)
.onAppear {
viewModel.getCheersList(creatorId: userId)
}
.padding(.horizontal, 13.3)
}
.onTapGesture { hideKeyboard() }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color(hex: "9970ff"))
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
ZStack {
if viewModel.isShowCheersReportView {
CheersReportDialogView(
isShowing: $viewModel.isShowCheersReportView,
confirmAction: { reason in
viewModel.report(type: .CHEERS, reason: reason)
}
)
}
if viewModel.isShowCheersDeleteView {
if viewModel.isShowCheersDeleteView {
SodaDialog(
title: "응원글 삭제",
desc: "삭제하시겠습니까?",
confirmButtonTitle: "삭제",
confirmButtonAction: {
viewModel.modifyCheers(cheersId: cheersId, creatorId: userId, isActive: false)
viewModel.isShowCheersDeleteView = false
self.cheersId = 0
},
cancelButtonTitle: "취소",
cancelButtonAction: { viewModel.isShowCheersDeleteView = false }
)
}
}
}
}
}
}
}

View File

@ -11,127 +11,221 @@ import Kingfisher
struct UserProfileFanTalkCheersItemView: View { struct UserProfileFanTalkCheersItemView: View {
let userId: Int let userId: Int
let cheer: GetCheersResponseItem let cheersItem: GetCheersResponseItem
let writeCheerReply: (String) -> Void let writeCheerReply: (String) -> Void
let modifyCheer: (Int, String) -> Void let modifyCheer: (Int, String) -> Void
let reportPopup: (Int) -> Void let reportPopup: (Int) -> Void
let onClickDelete: (Int) -> Void
@State var replyContent: String = "" @State var replyContent: String = ""
@State var isShowInputReply = false @State var isShowInputReply = false
@State var isShowPopupMenu: Bool = false
@State var isModeModify: Bool = false
@State var cheers: String = ""
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 0) { ZStack(alignment: .topTrailing) {
HStack(alignment: .top, spacing: 6.7) { VStack(alignment: .leading, spacing: 0) {
KFImage(URL(string: cheer.profileUrl)) HStack(alignment: .top, spacing: 6.7) {
.cancelOnDisappear(true) KFImage(URL(string: cheersItem.profileUrl))
.downsampling(size: CGSize(width: 33.3, height: 33.3)) .cancelOnDisappear(true)
.resizable() .downsampling(size: CGSize(width: 33.3, height: 33.3))
.frame(width: 33.3, height: 33.3) .resizable()
.clipShape(Circle()) .frame(width: 33.3, height: 33.3)
.clipShape(Circle())
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
Text("\(cheer.nickname)") Text("\(cheersItem.nickname)")
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
Text("\(cheer.date)") Text("\(cheersItem.date)")
.font(.custom(Font.medium.rawValue, size: 10.7)) .font(.custom(Font.medium.rawValue, size: 10.7))
.foregroundColor(Color(hex: "525252")) .foregroundColor(Color(hex: "525252"))
.padding(.top, 8.3) .padding(.top, 8.3)
Text("\(cheer.content)") if isModeModify {
.font(.custom(Font.medium.rawValue, size: 12)) HStack(spacing: 10) {
.foregroundColor(Color(hex: "777777")) TextField("", text: $cheers)
.padding(.top, 13.3) .autocapitalization(.none)
.disableAutocorrection(true)
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
.padding(13.3)
.background(Color(hex: "232323"))
.accentColor(Color(hex: "3bb9f1"))
.keyboardType(.default)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.strokeBorder(lineWidth: 1)
.foregroundColor(Color(hex: "9970ff"))
)
if isShowInputReply { Text("수정")
HStack(spacing: 10) { .font(.custom(Font.bold.rawValue, size: 13.3))
TextField("응원댓글에 답글을 남겨보세요!", text: $replyContent) .foregroundColor(Color(hex: "ffffff"))
.autocapitalization(.none) .padding(13.3)
.disableAutocorrection(true) .background(Color(hex: "9970ff"))
.font(.custom(Font.medium.rawValue, size: 13.3)) .cornerRadius(6.7)
.foregroundColor(Color(hex: "eeeeee"))
.padding(13.3)
.background(Color(hex: "232323"))
.accentColor(Color(hex: "9970ff"))
.keyboardType(.default)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.strokeBorder(lineWidth: 1)
.foregroundColor(Color(hex: "9970ff"))
)
Text("등록")
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color(hex: "ffffff"))
.padding(13.3)
.background(Color(hex: "9970ff"))
.cornerRadius(6.7)
.onTapGesture {
if cheer.replyList.count > 0 {
modifyCheer(cheer.replyList[0].cheersId, replyContent)
} else {
writeCheerReply(replyContent)
}
}
}
.padding(.top, 10)
} else {
if cheer.replyList.count <= 0 {
if userId == UserDefaults.int(forKey: .userId) {
Text("답글쓰기")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "9970ff"))
.padding(.top, 18.3)
.onTapGesture { .onTapGesture {
isShowInputReply = true modifyCheer(cheersItem.cheersId, cheers)
isModeModify = false
}
Text("취소")
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color(hex: "9970ff"))
.padding(13.3)
.background(Color(hex: "222222"))
.cornerRadius(6.7)
.onTapGesture {
isModeModify = false
} }
} }
.padding(.top, 13.3)
} else { } else {
let reply = cheer.replyList[0] Text("\(cheersItem.content)")
VStack(alignment: .leading, spacing: 8.3) { .font(.custom(Font.medium.rawValue, size: 12))
Text(reply.content) .foregroundColor(Color(hex: "777777"))
.font(.custom(Font.medium.rawValue, size: 12)) .padding(.top, 13.3)
}
if isShowInputReply {
HStack(spacing: 10) {
TextField("응원댓글에 답글을 남겨보세요!", text: $replyContent)
.autocapitalization(.none)
.disableAutocorrection(true)
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
.padding(13.3)
.background(Color(hex: "232323"))
.accentColor(Color(hex: "3bb9f1"))
.keyboardType(.default)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.strokeBorder(lineWidth: 1)
.foregroundColor(Color(hex: "3bb9f1"))
)
Text("등록")
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color(hex: "ffffff")) .foregroundColor(Color(hex: "ffffff"))
.frame(minWidth: 100) .padding(13.3)
.padding(.horizontal, 6.7) .background(Color(hex: "3bb9f1"))
.padding(.vertical, 6.7) .cornerRadius(6.7)
.background(Color(hex: "9970ff").opacity(0.3)) .onTapGesture {
.cornerRadius(16.7) if cheersItem.replyList.count > 0 {
.padding(.top, 18.3) modifyCheer(cheersItem.replyList[0].cheersId, replyContent)
} else {
writeCheerReply(replyContent)
}
}
}
.padding(.top, 10)
} else {
if cheersItem.replyList.count <= 0 {
if userId == UserDefaults.int(forKey: .userId) {
Text("답글쓰기")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "9970ff"))
.padding(.top, 18.3)
.onTapGesture {
isShowInputReply = true
}
}
} else {
let reply = cheersItem.replyList[0]
VStack(alignment: .leading, spacing: 8.3) {
Text(reply.content)
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "ffffff"))
.frame(minWidth: 100)
.padding(.horizontal, 6.7)
.padding(.vertical, 6.7)
.background(Color(hex: "9970ff").opacity(0.3))
.cornerRadius(16.7)
.padding(.top, 18.3)
HStack(spacing: 6.7) { HStack(spacing: 6.7) {
Text(reply.date) Text(reply.date)
.font(.custom(Font.medium.rawValue, size: 10.7))
.foregroundColor(Color(hex: "525252"))
if userId == UserDefaults.int(forKey: .userId) {
Text("답글 수정")
.font(.custom(Font.medium.rawValue, size: 10.7)) .font(.custom(Font.medium.rawValue, size: 10.7))
.foregroundColor(Color(hex: "9970ff")) .foregroundColor(Color(hex: "525252"))
.onTapGesture {
self.replyContent = reply.content if userId == UserDefaults.int(forKey: .userId) {
isShowInputReply = true Text("답글 수정")
} .font(.custom(Font.medium.rawValue, size: 10.7))
.foregroundColor(Color(hex: "9970ff"))
.onTapGesture {
self.replyContent = reply.content
isShowInputReply = true
}
}
} }
} }
} }
} }
} }
Spacer()
if !isModeModify {
Image("ic_seemore_vertical")
.onTapGesture { isShowPopupMenu = true }
}
} }
Spacer() Rectangle()
.frame(height: 1)
Image("ic_seemore_vertical") .foregroundColor(Color(hex: "909090").opacity(0.5))
.onTapGesture { reportPopup(cheer.cheersId) } .padding(.top, 13.3)
} }
.frame(width: screenSize().width - 26.7)
Rectangle() if isShowPopupMenu {
.frame(height: 1) VStack(spacing: 10) {
.foregroundColor(Color(hex: "909090").opacity(0.5)) if cheersItem.memberId != UserDefaults.int(forKey: .userId) {
.padding(.top, 13.3) Text("신고하기")
.font(.custom(Font.medium.rawValue, size: 14))
.foregroundColor(Color(hex: "777777"))
.onTapGesture {
reportPopup(cheersItem.cheersId)
isShowPopupMenu = false
}
}
if cheersItem.memberId == UserDefaults.int(forKey: .userId) {
Text("수정")
.font(.custom(Font.medium.rawValue, size: 14))
.foregroundColor(Color(hex: "777777"))
.onTapGesture {
isModeModify = true
isShowPopupMenu = false
cheers = cheersItem.content
}
}
if userId == UserDefaults.int(forKey: .userId) ||
cheersItem.memberId == UserDefaults.int(forKey: .userId)
{
Text("삭제")
.font(.custom(Font.medium.rawValue, size: 14))
.foregroundColor(Color(hex: "777777"))
.onTapGesture {
onClickDelete(cheersItem.cheersId)
isShowPopupMenu = false
}
}
}
.padding(10)
.background(Color(hex: "222222"))
}
}
.contentShape(Rectangle())
.onTapGesture {
isShowPopupMenu = false
} }
.frame(width: screenSize().width - 26.7)
} }
} }

View File

@ -16,6 +16,7 @@ struct UserProfileFanTalkView: View {
let cheers: GetCheersResponse let cheers: GetCheersResponse
let errorPopup: (String) -> Void let errorPopup: (String) -> Void
let reportPopup: (Int) -> Void let reportPopup: (Int) -> Void
let deletePopup: (Int) -> Void
@Binding var isLoading: Bool @Binding var isLoading: Bool
@State private var cheersContent: String = "" @State private var cheersContent: String = ""
@ -61,7 +62,7 @@ struct UserProfileFanTalkView: View {
.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(hex: "eeeeee"))
.accentColor(Color(hex: "9970ff")) .accentColor(Color(hex: "3bb9f1"))
.keyboardType(.default) .keyboardType(.default)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
@ -97,15 +98,18 @@ struct UserProfileFanTalkView: View {
let cheer = viewModel.cheersList[$0] let cheer = viewModel.cheersList[$0]
UserProfileFanTalkCheersItemView( UserProfileFanTalkCheersItemView(
userId: userId, userId: userId,
cheer: cheer, cheersItem: cheer,
writeCheerReply: { cheersReplyContent in writeCheerReply: { cheersReplyContent in
viewModel.writeCheersReply(parentCheersId: cheer.cheersId, creatorId: userId, cheersReplyContent: cheersReplyContent) viewModel.writeCheersReply(parentCheersId: cheer.cheersId, creatorId: userId, cheersReplyContent: cheersReplyContent)
}, },
modifyCheer: { cheersId, cheersReplyContent in modifyCheer: { cheersId, cheersReplyContent in
viewModel.modifyCheersReply(cheersId: cheersId, creatorId: userId, cheersReplyContent: cheersReplyContent) viewModel.modifyCheers(cheersId: cheersId, creatorId: userId, cheersContent: cheersReplyContent)
}, },
reportPopup: { cheersId in reportPopup: { cheersId in
reportPopup(cheersId) reportPopup(cheersId)
},
onClickDelete: { cheersId in
deletePopup(cheersId)
} }
) )
.onTapGesture { .onTapGesture {

View File

@ -17,8 +17,8 @@ final class UserProfileFanTalkViewModel: ObservableObject {
@Published var cheersList = [GetCheersResponseItem]() @Published var cheersList = [GetCheersResponseItem]()
@Published var reportCheersId = 0 @Published var reportCheersId = 0
@Published var isShowCheersReportMenu = false
@Published var isShowCheersReportView = false @Published var isShowCheersReportView = false
@Published var isShowCheersDeleteView = false
var errorPopup: ((String) -> Void)? var errorPopup: ((String) -> Void)?
var setLoading: ((Bool) -> Void)? var setLoading: ((Bool) -> Void)?
@ -224,8 +224,17 @@ final class UserProfileFanTalkViewModel: ObservableObject {
.store(in: &subscription) .store(in: &subscription)
} }
func modifyCheersReply(cheersId: Int, creatorId: Int, cheersReplyContent: String) { func modifyCheers(cheersId: Int, creatorId: Int, cheersContent: String? = nil, isActive: Bool? = nil) {
if cheersReplyContent.trimmingCharacters(in: .whitespaces).isEmpty { if cheersContent == nil && isActive == nil {
if let errorPopup = errorPopup {
errorPopup("변경사항이 없습니다.")
} else {
errorMessage = "변경사항이 없습니다."
isShowPopup = true
}
}
if let cheersContent = cheersContent, cheersContent.trimmingCharacters(in: .whitespaces).isEmpty {
if let errorPopup = errorPopup { if let errorPopup = errorPopup {
errorPopup("내용을 입력하세요") errorPopup("내용을 입력하세요")
} else { } else {
@ -241,7 +250,7 @@ final class UserProfileFanTalkViewModel: ObservableObject {
} }
isLoading = true isLoading = true
repository.modifyCheers(cheersId: cheersId, content: cheersReplyContent) repository.modifyCheers(cheersId: cheersId, content: cheersContent, isActive: isActive)
.sink { result in .sink { result in
switch result { switch result {
case .finished: case .finished:

View File

@ -14,6 +14,7 @@ struct GetCheersResponse: Decodable {
struct GetCheersResponseItem: Decodable { struct GetCheersResponseItem: Decodable {
let cheersId: Int let cheersId: Int
let memberId: Int
let nickname: String let nickname: String
let profileUrl: String let profileUrl: String
let content: String let content: String

View File

@ -0,0 +1,17 @@
//
// GetDonationAllResponse.swift
// SodaLive
//
// Created by klaus on 2023/08/29.
//
import Foundation
struct GetDonationAllResponse: Decodable {
let accumulatedCansToday: Int
let accumulatedCansLastWeek: Int
let accumulatedCansThisMonth: Int
let isVisibleDonationRank: Bool
let totalCount: Int
let userDonationRanking: [UserDonationRankingResponse]
}

View File

@ -0,0 +1,278 @@
//
// UserProfileDonationAllView.swift
// SodaLive
//
// Created by klaus on 2023/08/29.
//
import SwiftUI
import Kingfisher
struct UserProfileDonationAllView: View {
let userId: Int
@StateObject var viewModel = UserProfileDonationAllViewModel()
var body: some View {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) {
DetailNavigationBar(title: "후원랭킹 전체보기")
if userId == UserDefaults.int(forKey: .userId) {
VStack(spacing: 10.7) {
HStack(spacing: 10) {
Spacer()
Text("채널에 후원랭킹 활성화")
.font(.custom(Font.bold.rawValue, size: 16))
.foregroundColor(Color(hex: "eeeeee"))
Image(viewModel.isVisibleDonationRank ? "btn_toggle_on_big" : "btn_toggle_off_big")
.resizable()
.frame(width: 46.7, height: 27)
.onTapGesture {
viewModel.toggleVisibleDonationRank()
}
}
HStack(spacing: 0) {
Spacer()
Text("※ 비활성화하면 채널 내 후원랭킹이 표시되지 않으며,\n라이브 중에도 후원랭킹에 따른 뱃지가 반영되지 않습니다.")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "555555"))
.multilineTextAlignment(.trailing)
}
}
.padding(.top, 13.3)
.padding(.horizontal, 13.3)
VStack(spacing: 13.3) {
HStack(spacing: 0) {
Text("오늘")
.font(.custom(Font.bold.rawValue, size: 12))
.foregroundColor(Color(hex: "eeeeee"))
Spacer()
Text("\(viewModel.accumulatedCansToday.comma())")
.font(.custom(Font.bold.rawValue, size: 12))
.foregroundColor(Color(hex: "eeeeee"))
Text("")
.font(.custom(Font.light.rawValue, size: 12))
.foregroundColor(Color(hex: "eeeeee"))
}
HStack(spacing: 0) {
Text("지난주")
.font(.custom(Font.bold.rawValue, size: 12))
.foregroundColor(Color(hex: "eeeeee"))
Spacer()
Text("\(viewModel.accumulatedCansLastWeek.comma())")
.font(.custom(Font.bold.rawValue, size: 12))
.foregroundColor(Color(hex: "eeeeee"))
Text("")
.font(.custom(Font.light.rawValue, size: 12))
.foregroundColor(Color(hex: "eeeeee"))
}
HStack(spacing: 0) {
Text("이번 달 어제까지")
.font(.custom(Font.bold.rawValue, size: 12))
.foregroundColor(Color(hex: "eeeeee"))
Spacer()
Text("\(viewModel.accumulatedCansThisMonth.comma())")
.font(.custom(Font.bold.rawValue, size: 12))
.foregroundColor(Color(hex: "eeeeee"))
Text("")
.font(.custom(Font.light.rawValue, size: 12))
.foregroundColor(Color(hex: "eeeeee"))
}
}
.padding(16.7)
.background(Color(hex: "13181b"))
.cornerRadius(8)
.padding(.top, 13.3)
.padding(.horizontal, 13.3)
}
HStack(alignment: .center, spacing: 0) {
Text("전체")
.font(.custom(Font.medium.rawValue, size: 14.7))
.foregroundColor(Color(hex: "eeeeee"))
Text("\(viewModel.totalCount)")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "80d8ff"))
.padding(.leading, 6.7)
Text("")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "777777"))
Spacer()
}
.padding(.top, 13.3)
.padding(.horizontal, 13.3)
Rectangle()
.frame(width: screenSize().width - 26.7, height: 1)
.foregroundColor(Color(hex: "595959"))
.padding(.top, 6.7)
ScrollView(.vertical, showsIndicators: false) {
LazyVStack(spacing: 0) {
ForEach(0..<viewModel.userDonationRanking.count, id: \.self) { index in
let item = viewModel.userDonationRanking[index]
UserProfileDonationAllItemView(
index: index,
item: item,
withDonationCan: userId == UserDefaults.int(forKey: .userId),
itemCount: viewModel.userDonationRanking.count
)
.onAppear {
if index == viewModel.userDonationRanking.count - 1 {
viewModel.getCreatorProfileDonationRanking()
}
}
}
}
}
.padding(.vertical, 16.7)
.padding(.horizontal, 13.3)
}
.onAppear {
viewModel.userId = userId
viewModel.getCreatorProfileDonationRanking()
}
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color(hex: "9970ff"))
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
}
}
}
struct UserProfileDonationAllItemView: View {
let index: Int
let item: UserDonationRankingResponse
let withDonationCan: Bool
let itemCount: Int
let rankingCrawns = ["ic_crown_1", "ic_crown_2", "ic_crown_3"]
let rankingColors = [
[Color(hex: "ffdc00"), Color(hex: "ffb600")],
[Color(hex: "ffffff"), Color(hex: "9f9f9f")],
[Color(hex: "e6a77a"), Color(hex: "c67e4a")],
[Color(hex: "ffffff").opacity(0), Color(hex: "ffffff").opacity(0)]
]
var body: some View {
HStack(spacing: 0) {
ZStack {
KFImage(URL(string: item.profileImage))
.cancelOnDisappear(true)
.downsampling(size: CGSize(width: 60, height: 60))
.resizable()
.scaledToFill()
.frame(width: 60, height: 60, alignment: .top)
.clipShape(Circle())
.overlay(
Circle()
.stroke(
AngularGradient(colors: rankingColors[index < 4 ? index : 3], center: .center),
lineWidth: 3
)
)
if index < 3 {
VStack(alignment: .trailing, spacing: 0) {
Spacer()
Image(rankingCrawns[index])
.resizable()
.frame(width: 25, height: 25)
}
.frame(width: 63, height: 63, alignment: .trailing)
}
}
.frame(width: 63, height: 63)
Text("\(index + 1)")
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
.padding(.leading, 20)
.padding(.trailing, 13.3)
let nickname = item.nickname.count > 10 ? "\(String(item.nickname.prefix(10)))..." : item.nickname
Text(nickname)
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
Spacer()
if let donationCan = item.donationCan, donationCan > 0, withDonationCan {
Text("\(donationCan)")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
}
}
.padding(.horizontal, isTop3Index(index: index) ? 20 : 0)
.padding(.top, getTopPadding(index: index))
.padding(.bottom, getBottomPadding(index: index))
.background(Color(hex: "13181b").opacity(isTop3Index(index: index) ? 1 : 0))
.cornerRadius(4.7, corners: cornerRadiusConers(index: index))
.padding(.horizontal, isTop3Index(index: index) ? 0 : 6.7)
}
private func isTop3Index(index: Int) -> Bool {
return index == 0 || index == 1 || index == 2
}
private func getTopPadding(index: Int) -> CGFloat {
if index == 0 || index == 3 {
return 20
} else {
return 6.7
}
}
private func getBottomPadding(index: Int) -> CGFloat {
if (index == 0 && itemCount == 1) || (index == 1 && itemCount == 2) || index == 2 {
return 20
} else {
return 6.7
}
}
private func cornerRadiusConers(index: Int) -> UIRectCorner {
if (index == 0 && itemCount == 1) {
return [.topLeft, .topRight, .bottomLeft, .bottomRight]
} else if index == 0 {
return [.topLeft, .topRight]
} else if (index == 1 && itemCount == 2) || index == 2 {
return [.bottomLeft, .bottomRight]
} else {
return []
}
}
}

View File

@ -0,0 +1,125 @@
//
// UserProfileDonationAllViewModel.swift
// SodaLive
//
// Created by klaus on 2023/08/29.
//
import Foundation
import Combine
final class UserProfileDonationAllViewModel: ObservableObject {
private var repository = ExplorerRepository()
private var memberRepository = UserRepository()
private var subscription = Set<AnyCancellable>()
@Published var errorMessage = ""
@Published var isShowPopup = false
@Published var isLoading = false
@Published var totalCount = 0
@Published var accumulatedCansToday = 0
@Published var accumulatedCansLastWeek = 0
@Published var accumulatedCansThisMonth = 0
@Published var isVisibleDonationRank = false
@Published var userDonationRanking: [UserDonationRankingResponse] = []
var userId = 0
var page = 1
var isLast = false
private let pageSize = 10
func getCreatorProfileDonationRanking() {
if (!isLast && !isLoading) {
isLoading = true
repository.getCreatorProfileDonationRanking(userId: userId, page: page, size: pageSize)
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { [unowned self] response in
self.isLoading = false
let responseData = response.data
do {
let jsonDecoder = JSONDecoder()
let decoded = try jsonDecoder.decode(ApiResponse<GetDonationAllResponse>.self, from: responseData)
if let data = decoded.data, decoded.success {
self.accumulatedCansToday = data.accumulatedCansToday
self.accumulatedCansLastWeek = data.accumulatedCansLastWeek
self.accumulatedCansThisMonth = data.accumulatedCansThisMonth
self.isVisibleDonationRank = data.isVisibleDonationRank
if !data.userDonationRanking.isEmpty {
page += 1
self.totalCount = data.totalCount
self.userDonationRanking.append(contentsOf: data.userDonationRanking)
} else {
isLast = true
}
} else {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
}
self.isShowPopup = true
}
} catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
}
}
.store(in: &subscription)
}
}
func toggleVisibleDonationRank() {
isLoading = true
memberRepository.profileUpdate(
request: ProfileUpdateRequest(
email: UserDefaults.string(forKey: .email),
isVisibleDonationRank: !isVisibleDonationRank
)
)
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { [unowned self] response in
self.isLoading = false
let responseData = response.data
do {
let jsonDecoder = JSONDecoder()
let decoded = try jsonDecoder.decode(ApiResponse<GetProfileResponse>.self, from: responseData)
if decoded.success {
isVisibleDonationRank.toggle()
} else {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
}
self.isShowPopup = true
}
} catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
}
}
.store(in: &subscription)
}
}

View File

@ -32,7 +32,9 @@ struct UserProfileDonationView: View {
Text("전체보기") Text("전체보기")
.font(.custom(Font.light.rawValue, size: 11.3)) .font(.custom(Font.light.rawValue, size: 11.3))
.foregroundColor(Color(hex: "bbbbbb")) .foregroundColor(Color(hex: "bbbbbb"))
.onTapGesture {} .onTapGesture {
AppState.shared.setAppStep(step: .userProfileDonationAll(userId: userId))
}
} }
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {

View File

@ -45,46 +45,48 @@ struct UserProfileView: View {
ScrollView(.vertical, showsIndicators: false) { ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 0) { VStack(spacing: 0) {
if let creatorProfile = viewModel.creatorProfile { if let creatorProfile = viewModel.creatorProfile {
UserProfileCreatorView( VStack(spacing: 0) {
creator: creatorProfile.creator) { UserProfileCreatorView(
viewModel.creatorFollow() creator: creatorProfile.creator) {
} creatorUnFollow: { viewModel.creatorFollow()
viewModel.creatorUnFollow() } creatorUnFollow: {
} shareChannel: { viewModel.creatorUnFollow()
viewModel.shareChannel(userId: userId) } shareChannel: {
} viewModel.shareChannel(userId: userId)
}
UserProfileActivitySummaryView(item: creatorProfile.activitySummary) UserProfileActivitySummaryView(item: creatorProfile.activitySummary)
.padding(.top, 13.3)
.padding(.horizontal, 13.3)
HStack(spacing: 0) {
Text(
creatorProfile.notice.trimmingCharacters(in: .whitespaces).isEmpty ?
"공지사항이 없습니다." :
creatorProfile.notice
)
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "000000"))
.lineLimit(viewModel.isExpandNotice ? Int.max : 1)
Spacer()
if creatorProfile.creator.creatorId == UserDefaults.int(forKey: .userId) {
Image("ic_review")
.resizable()
.frame(width: 20, height: 20)
}
}
.padding(.horizontal, 26.7)
.padding(.vertical, 13.3)
.background(Color(hex: "fdca2f"))
.padding(.top, 13.3) .padding(.top, 13.3)
.padding(.horizontal, 13.3) .onTapGesture {
if creatorProfile.creator.creatorId == UserDefaults.int(forKey: .userId) {
HStack(spacing: 0) { AppState.shared.setAppStep(step: .creatorNoticeWrite(notice: creatorProfile.notice))
Text( } else {
creatorProfile.notice.trimmingCharacters(in: .whitespaces).isEmpty ? viewModel.isExpandNotice.toggle()
"공지사항이 없습니다." : }
creatorProfile.notice
)
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "000000"))
.lineLimit(viewModel.isExpandNotice ? Int.max : 1)
Spacer()
if creatorProfile.creator.creatorId == UserDefaults.int(forKey: .userId) {
Image("ic_review")
.resizable()
.frame(width: 20, height: 20)
}
}
.padding(.horizontal, 26.7)
.padding(.vertical, 13.3)
.background(Color(hex: "fdca2f"))
.padding(.top, 13.3)
.onTapGesture {
if creatorProfile.creator.creatorId == UserDefaults.int(forKey: .userId) {
AppState.shared.setAppStep(step: .creatorNoticeWrite(notice: creatorProfile.notice))
} else {
viewModel.isExpandNotice.toggle()
} }
} }
@ -95,7 +97,7 @@ struct UserProfileView: View {
userId: userId, userId: userId,
items: creatorProfile.contentList items: creatorProfile.contentList
) )
.padding(.top, 46.7) .padding(.top, 26.7)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
} }
@ -123,7 +125,7 @@ struct UserProfileView: View {
} }
} }
) )
.padding(.top, 46.7) .padding(.top, 26.7)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
} }
@ -153,33 +155,41 @@ struct UserProfileView: View {
.padding(.top, 26.7) .padding(.top, 26.7)
} }
VStack(spacing: 26.7) { VStack(spacing: 0) {
UserProfileSimilarCreatorView( if creatorProfile.similarCreatorList.count > 0 {
creators: creatorProfile.similarCreatorList, VStack(spacing: 26.7) {
onClickCreator: { viewModel.getCreatorProfile(userId: $0) } UserProfileSimilarCreatorView(
creators: creatorProfile.similarCreatorList,
onClickCreator: { viewModel.getCreatorProfile(userId: $0) }
)
.padding(.horizontal, 13.3)
Rectangle()
.frame(height: 6.7)
.foregroundColor(Color(hex: "909090").opacity(0.5))
}
.padding(.top, 26.7)
}
UserProfileFanTalkView(
userId: userId,
cheers: creatorProfile.cheers,
errorPopup: { message in
viewModel.errorMessage = message
viewModel.isShowPopup = true
},
reportPopup: { cheerId in
viewModel.cheersId = cheerId
viewModel.isShowCheersReportView = true
},
deletePopup: { cheerId in
viewModel.cheersId = cheerId
viewModel.isShowCheersDeleteView = true
},
isLoading: $viewModel.isLoading
) )
.padding(.horizontal, 13.3) .padding(.top, 26.7)
Rectangle()
.frame(height: 6.7)
.foregroundColor(Color(hex: "909090").opacity(0.5))
} }
.padding(.top, 26.7)
UserProfileFanTalkView(
userId: userId,
cheers: creatorProfile.cheers,
errorPopup: { message in
viewModel.errorMessage = message
viewModel.isShowPopup = true
},
reportPopup: { cheerId in
viewModel.reportCheersId = cheerId
viewModel.isShowCheersReportMenu = true
},
isLoading: $viewModel.isLoading
)
.padding(.top, 26.7)
} }
} }
} }
@ -220,20 +230,18 @@ struct UserProfileView: View {
) )
} }
if viewModel.isShowCheersReportMenu { if viewModel.isShowCheersDeleteView {
VStack(spacing: 0) { SodaDialog(
CheersReportMenuView( title: "응원글 삭제",
isShowing: $viewModel.isShowCheersReportMenu, desc: "삭제하시겠습니까?",
onClickReport: { viewModel.isShowCheersReportView = true } confirmButtonTitle: "삭제",
) confirmButtonAction: {
viewModel.deleteCheers(creatorId: userId)
if proxy.safeAreaInsets.bottom > 0 { viewModel.isShowCheersDeleteView = false
Rectangle() },
.foregroundColor(Color(hex: "222222")) cancelButtonTitle: "취소",
.frame(width: proxy.size.width, height: 15.3) cancelButtonAction: { viewModel.isShowCheersDeleteView = false }
} )
}
.ignoresSafeArea()
} }
if viewModel.isShowCheersReportView { if viewModel.isShowCheersReportView {

View File

@ -47,10 +47,9 @@ final class UserProfileViewModel: ObservableObject {
@Published var isShowUesrReportView = false @Published var isShowUesrReportView = false
@Published var isShowProfileReportConfirm = false @Published var isShowProfileReportConfirm = false
@Published var reportCheersId = 0 @Published var cheersId = 0
@Published var isShowCheersReportMenu = false
@Published var isShowCheersReportView = false @Published var isShowCheersReportView = false
@Published var isShowCheersDeleteView = false
let paymentDialogCancelTitle = "취소" let paymentDialogCancelTitle = "취소"
@ -488,7 +487,7 @@ final class UserProfileViewModel: ObservableObject {
func report(type: ReportType, userId: Int? = nil, reason: String = "프로필 신고") { func report(type: ReportType, userId: Int? = nil, reason: String = "프로필 신고") {
isLoading = true isLoading = true
let request = ReportRequest(type: type, reason: reason, reportedMemberId: userId, cheersId: reportCheersId > 0 && type == .CHEERS ? reportCheersId : nil, audioContentId: nil) let request = ReportRequest(type: type, reason: reason, reportedMemberId: userId, cheersId: cheersId > 0 && type == .CHEERS ? cheersId : nil, audioContentId: nil)
reportRepository.report(request: request) reportRepository.report(request: request)
.sink { result in .sink { result in
switch result { switch result {
@ -501,7 +500,7 @@ final class UserProfileViewModel: ObservableObject {
self.isLoading = false self.isLoading = false
let responseData = response.data let responseData = response.data
self.reportCheersId = 0 self.cheersId = 0
do { do {
let jsonDecoder = JSONDecoder() let jsonDecoder = JSONDecoder()
@ -521,4 +520,43 @@ final class UserProfileViewModel: ObservableObject {
} }
.store(in: &subscription) .store(in: &subscription)
} }
func deleteCheers(creatorId: Int) {
isLoading = true
repository.modifyCheers(cheersId: cheersId, content: nil, isActive: false)
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { [unowned self] response in
self.cheersId = 0
self.isLoading = false
let responseData = response.data
do {
let jsonDecoder = JSONDecoder()
let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
if decoded.success {
self.getCreatorProfile(userId: creatorId)
} else {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
}
self.isShowPopup = true
}
} catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
}
}
.store(in: &subscription)
}
} }

View File

@ -30,3 +30,15 @@ extension Int64 {
return timeText.joined(separator: ":") return timeText.joined(separator: ":")
} }
} }
extension Int {
func comma() -> String {
let numberFormatter = NumberFormatter()
numberFormatter.groupingSeparator = ","
numberFormatter.groupingSize = 3
numberFormatter.usesGroupingSeparator = true
numberFormatter.decimalSeparator = "."
numberFormatter.numberStyle = .decimal
return numberFormatter.string(from: self as NSNumber)!
}
}

View File

@ -17,6 +17,7 @@ enum UserDefaultsKey: String, CaseIterable {
case nickname case nickname
case pushToken case pushToken
case profileImage case profileImage
case noChatRoomList
case devicePushToken case devicePushToken
case isContentPlayLoop case isContentPlayLoop
case isFollowedChannel case isFollowedChannel

View File

@ -32,6 +32,7 @@ struct FollowCreatorView: View {
Spacer() Spacer()
} }
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
.padding(.top, 6.7)
ScrollView(.vertical, showsIndicators: false) { ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 13.3) { VStack(spacing: 13.3) {

View File

@ -29,6 +29,7 @@ struct LiveView: View {
if viewModel.recommendLiveItems.count > 0 { if viewModel.recommendLiveItems.count > 0 {
SectionRecommendLiveView(items: viewModel.recommendLiveItems) SectionRecommendLiveView(items: viewModel.recommendLiveItems)
.padding(.top, 13.3) .padding(.top, 13.3)
.padding(.bottom, 40)
} }
if viewModel.recommendChannelItems.count > 0 { if viewModel.recommendChannelItems.count > 0 {
@ -38,21 +39,18 @@ struct LiveView: View {
viewModel.recommendChannelItems, viewModel.recommendChannelItems,
isFollowingList: $viewModel.isFollowingList isFollowingList: $viewModel.isFollowingList
) )
.padding(.top, 40)
} }
if viewModel.liveNowItems.count > 0 { SectionLiveNowView(
SectionLiveNowView( items: viewModel.liveNowItems,
items: viewModel.liveNowItems, onClickParticipant: {
onClickParticipant: { viewModel.enterLiveRoom(roomId: $0)
viewModel.enterRoom(roomId: $0) },
}, onTapCreateLive: {
onTapCreateLive: { AppState.shared.setAppStep(step: .createLive(timeSettingMode: .NOW, onSuccess: onCreateSuccess))
AppState.shared.setAppStep(step: .createLive(timeSettingMode: .NOW, onSuccess: onCreateSuccess)) }
} )
) .padding(.top, 40)
.padding(.top, 40)
}
if viewModel.eventBannerItems.count > 0 { if viewModel.eventBannerItems.count > 0 {
SectionEventBannerView(items: viewModel.eventBannerItems) SectionEventBannerView(items: viewModel.eventBannerItems)
@ -64,20 +62,18 @@ struct LiveView: View {
.padding(.top, 40) .padding(.top, 40)
} }
if viewModel.liveReservationItems.count > 0 { SectionLiveReservationView(
SectionLiveReservationView( items: viewModel.liveReservationItems,
items: viewModel.liveReservationItems, onClickCancel: { viewModel.getSummary() },
onClickCancel: { viewModel.getSummary() }, onClickStart: { roomId in processStart(roomId: roomId) },
onClickStart: { roomId in processStart(roomId: roomId) }, onClickReservation: { roomId in
onClickReservation: { roomId in viewModel.reservationLiveRoom(roomId: roomId)
viewModel.reservationLiveRoom(roomId: roomId) },
}, onTapCreateLive: {
onTapCreateLive: { AppState.shared.setAppStep(step: .createLive(timeSettingMode: .RESERVATION, onSuccess: onCreateSuccess))
AppState.shared.setAppStep(step: .createLive(timeSettingMode: .RESERVATION, onSuccess: onCreateSuccess)) }
} )
) .padding(.top, 40)
.padding(.top, 40)
}
} }
} }
.frame(width: geo.size.width, height: geo.size.height) .frame(width: geo.size.width, height: geo.size.height)
@ -141,44 +137,6 @@ struct LiveView: View {
} }
} }
} }
.valueChanged(value: appState.pushRoomId) { value in
DispatchQueue.main.async {
appState.setAppStep(step: .main)
if value > 0 {
viewModel.enterRoom(roomId: value)
}
}
}
.valueChanged(value: appState.pushChannelId) { value in
DispatchQueue.main.async {
appState.setAppStep(step: .main)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if value > 0 {
appState.setAppStep(step: .creatorDetail(userId: value))
}
}
}
}
.valueChanged(value: appState.pushAudioContentId) { value in
appState.setAppStep(step: .main)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if value > 0 {
appState.setAppStep(step: .contentDetail(contentId: value))
}
}
}
.onAppear {
if appState.pushRoomId > 0 {
viewModel.enterRoom(roomId: appState.pushRoomId)
} else if appState.pushChannelId > 0 {
appState.setAppStep(step: .creatorDetail(userId: appState.pushChannelId))
} else if appState.pushAudioContentId > 0 {
appState.setAppStep(step: .contentDetail(contentId: appState.pushAudioContentId))
}
}
} }
private func onCreateSuccess(response: CreateLiveRoomResponse) { private func onCreateSuccess(response: CreateLiveRoomResponse) {

View File

@ -404,6 +404,56 @@ final class LiveViewModel: ObservableObject {
} }
} }
func enterLiveRoom(roomId: Int) {
getRoomDetail(roomId: roomId) {
if let _ = $0.channelName {
if $0.manager.id == UserDefaults.int(forKey: .userId) {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) {
self.enterRoom(roomId: roomId)
}
} else if ($0.price == 0 || $0.isPaid) {
if $0.isPrivateRoom {
self.passwordDialogConfirmAction = { password in
self.enterRoom(roomId: roomId, password: password)
}
self.isShowPasswordDialog = true
} else {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) {
self.enterRoom(roomId: roomId)
}
}
} else {
if $0.isPrivateRoom {
self.secretOrPasswordDialogCan = $0.price
self.passwordDialogConfirmAction = { password in
self.enterRoom(roomId: roomId, password: password)
}
self.isShowPasswordDialog = true
} else {
self.paymentDialogTitle = "\($0.price)캔으로 입장"
self.paymentDialogDesc = "'\($0.title)' 라이브에 참여하기 위해 결제합니다."
self.paymentDialogConfirmTitle = "결제 후 참여하기"
self.paymentDialogConfirmAction = { [unowned self] in
hidePopup()
self.enterRoom(roomId: roomId)
}
self.isShowPaymentDialog = true
}
}
} else {
AppState.shared.setAppStep(
step: .liveDetail(
roomId: roomId,
onClickParticipant: {},
onClickReservation: { self.reservationLiveRoom(roomId: roomId) },
onClickStart: { self.startLive(roomId: roomId) },
onClickCancel: { self.getSummary() }
)
)
}
}
}
private func getRoomDetail(roomId: Int, onSuccess: @escaping (GetRoomDetailResponse) -> Void) { private func getRoomDetail(roomId: Int, onSuccess: @escaping (GetRoomDetailResponse) -> Void) {
isLoading = true isLoading = true
repository.getRoomDetail(roomId: roomId) repository.getRoomDetail(roomId: roomId)

View File

@ -70,31 +70,16 @@ struct SectionLiveNowView: View {
.resizable() .resizable()
.frame(width: 60, height: 60) .frame(width: 60, height: 60)
Text("🙀지금 참여가능한 라이브가 없습니다.\n직접 라이브를 만들어 보세요!") Text("🙀지금 참여가능한 라이브가 없습니다.\n채널을 팔로잉 하고 라이브 알림을 받아 보세요.")
.font(.custom(Font.medium.rawValue, size: 10.7)) .font(.custom(Font.medium.rawValue, size: 10.7))
.foregroundColor(Color(hex: "bbbbbb")) .foregroundColor(Color(hex: "bbbbbb"))
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.padding(.top, 8) .padding(.vertical, 8)
HStack(spacing: 0) {
Image("ic_plus_no_bg")
.resizable()
.frame(width: 33.3, height: 33.3, alignment: .center)
Text("라이브 만들기")
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color.white)
}
.frame(width: 200, height: 33.3, alignment: .center)
.background(Color(hex: "9970ff"))
.cornerRadius(4.7)
.padding(.top, 10.7)
.onTapGesture { onTapCreateLive() }
} }
.padding(.vertical, 16.7) .padding(.vertical, 16.7)
.frame(width: screenSize().width - 26.7) .frame(width: screenSize().width - 26.7)
.background(Color(hex: "2b2635")) .background(Color(hex: "13181b"))
.cornerRadius(4.7) .cornerRadius(4.7)
.padding(.top, 28.3) .padding(.top, 28.3)
} }

View File

@ -39,6 +39,7 @@ struct SectionRecommendLiveView: View {
height: (screenSize().width - 26.7) * 0.53 height: (screenSize().width - 26.7) * 0.53
) )
.onTapGesture { .onTapGesture {
AppState.shared.setAppStep(step: .creatorDetail(userId: item.creatorId))
} }
.cornerRadius(4.7) .cornerRadius(4.7)
} else { } else {
@ -50,6 +51,7 @@ struct SectionRecommendLiveView: View {
height: (screenSize().width - 26.7) * 0.53 height: (screenSize().width - 26.7) * 0.53
) )
.onTapGesture { .onTapGesture {
AppState.shared.setAppStep(step: .creatorDetail(userId: item.creatorId))
} }
.cornerRadius(4.7) .cornerRadius(4.7)
} }

View File

@ -83,7 +83,7 @@ struct LiveReservationAllView: View {
} }
.padding(.vertical, 16.7) .padding(.vertical, 16.7)
.frame(width: screenSize().width - 26.7) .frame(width: screenSize().width - 26.7)
.background(Color(hex: "2b2635")) .background(Color(hex: "13181b"))
.cornerRadius(4.7) .cornerRadius(4.7)
.padding(.top, 28.3) .padding(.top, 28.3)
} }

View File

@ -33,7 +33,7 @@ struct WeekCalendarView: View {
} }
.frame(width: 53.3) .frame(width: 53.3)
.frame(minHeight: 66.7) .frame(minHeight: 66.7)
.background(index == selectedIndex ? Color(hex: "9970ff") : Color.clear) .background(index == selectedIndex ? Color(hex: "3bb9f1") : Color.clear)
.cornerRadius(6.7) .cornerRadius(6.7)
.onTapGesture { .onTapGesture {
if self.selectedIndex != index { if self.selectedIndex != index {

View File

@ -21,7 +21,7 @@ struct MyLiveReservationItemView: View {
Text("내가 개설한 라이브") Text("내가 개설한 라이브")
.font(.custom(Font.bold.rawValue, size: 16)) .font(.custom(Font.bold.rawValue, size: 16))
.foregroundColor(Color(hex: "9970ff")) .foregroundColor(Color(hex: "3bb9f1"))
} }
} }
@ -66,7 +66,7 @@ struct MyLiveReservationItemView: View {
} }
.overlay( .overlay(
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: 8)
.stroke(Color(hex: "9970ff"), lineWidth: 1) .stroke(Color(hex: "3bb9f1"), lineWidth: 1)
) )
Divider() Divider()

View File

@ -25,7 +25,7 @@ struct SectionLiveReservationView: View {
Text("예약중") Text("예약중")
.font(.custom(Font.bold.rawValue, size: 18.3)) .font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "9970ff")) .foregroundColor(Color(hex: "3bb9f1"))
Spacer() Spacer()
@ -75,7 +75,7 @@ struct SectionLiveReservationView: View {
step: .liveDetail( step: .liveDetail(
roomId: item.roomId, roomId: item.roomId,
onClickParticipant: {}, onClickParticipant: {},
onClickReservation: {}, onClickReservation: { onClickReservation(item.roomId) },
onClickStart: { onClickStart(item.roomId) }, onClickStart: { onClickStart(item.roomId) },
onClickCancel: onClickCancel onClickCancel: onClickCancel
) )
@ -93,31 +93,16 @@ struct SectionLiveReservationView: View {
.resizable() .resizable()
.frame(width: 60, height: 60) .frame(width: 60, height: 60)
Text("지금 예약중인 라이브가 없습니다.\n직접 라이브를 만들어 보세요!") Text("지금 예약중인 라이브가 없습니다.\n채널을 팔로잉 하고 라이브 알림을 받아 보세요.")
.font(.custom(Font.medium.rawValue, size: 10.7)) .font(.custom(Font.medium.rawValue, size: 10.7))
.foregroundColor(Color(hex: "bbbbbb")) .foregroundColor(Color(hex: "bbbbbb"))
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.padding(.top, 8) .padding(.vertical, 8)
HStack(spacing: 0) {
Image("ic_plus_no_bg")
.resizable()
.frame(width: 33.3, height: 33.3, alignment: .center)
Text("라이브 만들기")
.font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color.white)
}
.frame(width: 200, height: 33.3, alignment: .center)
.background(Color(hex: "9970ff"))
.cornerRadius(4.7)
.padding(.top, 10.7)
.onTapGesture { onTapCreateLive() }
} }
.padding(.vertical, 16.7) .padding(.vertical, 16.7)
.frame(width: screenSize().width - 26.7) .frame(width: screenSize().width - 26.7)
.background(Color(hex: "2b2635")) .background(Color(hex: "13181b"))
.cornerRadius(4.7) .cornerRadius(4.7)
.padding(.top, 28.3) .padding(.top, 28.3)
} }

View File

@ -262,7 +262,7 @@ struct LiveRoomCreateView: View {
.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(hex: "eeeeee"))
.accentColor(Color(hex: "9970ff")) .accentColor(Color(hex: "3bb9f1"))
.keyboardType(.default) .keyboardType(.default)
.padding(.top, 12) .padding(.top, 12)
.padding(.horizontal, 6.7) .padding(.horizontal, 6.7)
@ -480,7 +480,7 @@ struct LiveRoomCreateView: View {
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.font(.custom(Font.medium.rawValue, size: 14.7)) .font(.custom(Font.medium.rawValue, size: 14.7))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
.accentColor(Color(hex: "9970ff")) .accentColor(Color(hex: "3bb9f1"))
.keyboardType(.numberPad) .keyboardType(.numberPad)
.padding(.vertical, 15.7) .padding(.vertical, 15.7)
.frame(width: screenSize().width - 26.7, alignment: .center) .frame(width: screenSize().width - 26.7, alignment: .center)
@ -626,7 +626,7 @@ struct LiveRoomCreateView: View {
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.font(.custom(Font.medium.rawValue, size: 14.7)) .font(.custom(Font.medium.rawValue, size: 14.7))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
.accentColor(Color(hex: "9970ff")) .accentColor(Color(hex: "3bb9f1"))
.keyboardType(.numberPad) .keyboardType(.numberPad)
.padding(.vertical, 15.7) .padding(.vertical, 15.7)
.frame(width: screenSize().width - 26.7, alignment: .center) .frame(width: screenSize().width - 26.7, alignment: .center)
@ -720,7 +720,7 @@ struct LiveRoomCreateView: View {
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.font(.custom(Font.bold.rawValue, size: 13.3)) .font(.custom(Font.bold.rawValue, size: 13.3))
.foregroundColor(Color(hex: "9970ff")) .foregroundColor(Color(hex: "9970ff"))
.accentColor(Color(hex: "9970ff")) .accentColor(Color(hex: "3bb9f1"))
.keyboardType(.numberPad) .keyboardType(.numberPad)
Spacer() Spacer()

View File

@ -305,11 +305,6 @@ struct LiveDetailView: View {
if room.manager.id == UserDefaults.int(forKey: .userId) { if room.manager.id == UserDefaults.int(forKey: .userId) {
VStack(spacing: 16.7) { VStack(spacing: 16.7) {
HStack(spacing: 13.3) { HStack(spacing: 13.3) {
Image("btn_big_share")
.onTapGesture {
viewModel.shareRoom(roomId: room.roomId)
}
Text("수정") Text("수정")
.font(.custom(Font.bold.rawValue, size: 18.3)) .font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color.white) .foregroundColor(Color.white)
@ -355,65 +350,41 @@ struct LiveDetailView: View {
} }
.frame(width: screenSize().width - 26.7) .frame(width: screenSize().width - 26.7)
} else if room.isPaid { } else if room.isPaid {
HStack(spacing: 13.3) { Text("예약완료")
Button { .font(.custom(Font.bold.rawValue, size: 18.3))
viewModel.shareRoom(roomId: room.roomId) .foregroundColor(Color(hex: "777777"))
} label: { .padding(.vertical, 16)
Image("btn_big_share") .padding(.horizontal, 99)
} .background(Color(hex: "525252"))
.cornerRadius(10)
Text("예약완료") .frame(width: screenSize().width - 26.7)
} else {
Button {
onClickReservation()
AppState.shared.back()
} label: {
Text("예약하기")
.font(.custom(Font.bold.rawValue, size: 18.3)) .font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color.white)
.padding(.vertical, 16) .padding(.vertical, 16)
.padding(.horizontal, 99) .padding(.horizontal, 99)
.background(Color(hex: "525252")) .background(Color(hex: "9970ff"))
.cornerRadius(10) .cornerRadius(10)
} }
.frame(width: screenSize().width - 26.7) .frame(width: screenSize().width - 26.7)
} else {
HStack(spacing: 13.3) {
Button {
viewModel.shareRoom(roomId: room.roomId)
} label: {
Image("btn_big_share")
}
Button {
onClickReservation()
AppState.shared.back()
} label: {
Text("예약하기")
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color.white)
.padding(.vertical, 16)
.padding(.horizontal, 99)
.background(Color(hex: "9970ff"))
.cornerRadius(10)
}
}
.frame(width: screenSize().width - 26.7)
} }
} else { } else {
HStack(spacing: 13.3) { Button {
Button { onClickParticipant()
viewModel.shareRoom(roomId: room.roomId) AppState.shared.back()
} label: { } label: {
Image("btn_big_share") Text("지금 참여하기")
} .font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color.white)
Button { .padding(.vertical, 16)
onClickParticipant() .padding(.horizontal, 79)
AppState.shared.back() .background(Color(hex: "ff5c49"))
} label: { .cornerRadius(10)
Text("지금 참여하기")
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color.white)
.padding(.vertical, 16)
.padding(.horizontal, 79)
.background(Color(hex: "ff5c49"))
.cornerRadius(10)
}
} }
.frame(width: screenSize().width - 26.7) .frame(width: screenSize().width - 26.7)
} }

View File

@ -75,7 +75,7 @@ struct LiveRoomDonationRankingItemView: View {
.padding(.horizontal, isTop3Index(index: index) ? 20 : 0) .padding(.horizontal, isTop3Index(index: index) ? 20 : 0)
.padding(.top, getTopPadding(index: index)) .padding(.top, getTopPadding(index: index))
.padding(.bottom, getBottomPadding(index: index)) .padding(.bottom, getBottomPadding(index: index))
.background(Color(hex: "2b2635").opacity(isTop3Index(index: index) ? 1 : 0)) .background(Color(hex: "13181b").opacity(isTop3Index(index: index) ? 1 : 0))
.cornerRadius(4.7, corners: cornerRadiusConers(index: index)) .cornerRadius(4.7, corners: cornerRadiusConers(index: index))
.padding(.horizontal, isTop3Index(index: index) ? 0 : 6.7) .padding(.horizontal, isTop3Index(index: index) ? 0 : 6.7)
} }

View File

@ -30,7 +30,7 @@ struct LiveRoomDonationRankingTotalCanView: View {
} }
.padding(.horizontal, 18.7) .padding(.horizontal, 18.7)
.padding(.vertical, 10.7) .padding(.vertical, 10.7)
.background(Color(hex: "2b2635")) .background(Color(hex: "13181b"))
.cornerRadius(8) .cornerRadius(8)
} }
} }

View File

@ -194,7 +194,7 @@ struct LiveRoomInfoEditDialog: View {
.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(hex: "eeeeee"))
.accentColor(Color(hex: "9970ff")) .accentColor(Color(hex: "3bb9f1"))
.keyboardType(.default) .keyboardType(.default)
.padding(.top, 12) .padding(.top, 12)
.padding(.horizontal, 6.7) .padding(.horizontal, 6.7)

View File

@ -0,0 +1,85 @@
//
// LiveRoomNoChattingDialogView.swift
// SodaLive
//
// Created by klaus on 2023/10/11.
//
import SwiftUI
import Kingfisher
struct LiveRoomNoChattingDialogView: View {
let nickname: String
let profileUrl: String
let confirmAction: () -> Void
let cancelAction: () -> Void
var body: some View {
ZStack {
Color.black
.opacity(0.5)
.frame(width: screenSize().width, height: screenSize().height)
VStack(spacing: 21) {
Text("채팅금지")
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "bbbbbb"))
HStack(spacing: 8) {
KFImage(URL(string: profileUrl))
.resizable()
.frame(width: 26.7, height: 26.7)
Text(nickname)
.font(.custom(Font.medium.rawValue, size: 16.7))
.foregroundColor(Color(hex: "bbbbbb"))
}
Text("3분간 채팅금지를 하겠습니까?")
.font(.custom(Font.medium.rawValue, size: 15))
.foregroundColor(Color(hex: "bbbbbb"))
HStack(spacing: 13.3) {
Text("취소")
.font(.custom(Font.bold.rawValue, size: 15.3))
.foregroundColor(Color(hex: "9970ff"))
.padding(.vertical, 16)
.frame(width: (screenSize().width - 80) / 2)
.background(Color(hex: "9970ff").opacity(0.13))
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color(hex: "9970ff"), lineWidth: 1)
)
.onTapGesture { cancelAction() }
Text("확인")
.font(.custom(Font.bold.rawValue, size: 15.3))
.foregroundColor(Color(hex: "ffffff"))
.padding(.vertical, 16)
.frame(width: (screenSize().width - 80) / 2)
.background(Color(hex: "9970ff"))
.cornerRadius(8)
.onTapGesture { confirmAction() }
}
}
.padding(.top, 40)
.padding(.bottom, 16.7)
.padding(.horizontal, 16.7)
.background(Color(hex: "222222"))
.cornerRadius(10)
}
}
}
struct LiveRoomNoChattingDialogView_Previews: PreviewProvider {
static var previews: some View {
LiveRoomNoChattingDialogView(
nickname: "닉네임",
profileUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
confirmAction: {},
cancelAction: {}
)
}
}

View File

@ -74,6 +74,7 @@ struct LiveRoomProfileItemMasterView: View {
struct LiveRoomProfileItemUserView: View { struct LiveRoomProfileItemUserView: View {
let isStaff: Bool let isStaff: Bool
let userId: Int let userId: Int
let creatorId: Int
let nickname: String let nickname: String
let profileUrl: String let profileUrl: String
let role: LiveRoomMemberRole let role: LiveRoomMemberRole
@ -82,6 +83,7 @@ struct LiveRoomProfileItemUserView: View {
let onClickInviteSpeaker: (Int) -> Void let onClickInviteSpeaker: (Int) -> Void
let onClickKickOut: (Int) -> Void let onClickKickOut: (Int) -> Void
let onClickProfile: (Int) -> Void let onClickProfile: (Int) -> Void
let onClickNoChatting: (Int, String, String) -> Void
var body: some View { var body: some View {
ZStack { ZStack {
@ -145,6 +147,25 @@ struct LiveRoomProfileItemUserView: View {
} }
} }
if role != .MANAGER && creatorId == UserDefaults.int(forKey: .userId) {
Text("채금")
.font(.custom(Font.medium.rawValue, size: 10))
.foregroundColor(Color(hex: "ffffff"))
.padding(.horizontal, 5.5)
.padding(.vertical, 12)
.background(Color(hex: "9970ff").opacity(0.3))
.cornerRadius(6.7)
.overlay(
RoundedRectangle(cornerRadius: 6.7)
.stroke(Color(hex: "9970ff"), lineWidth: 1)
)
.cornerRadius(6.7)
.padding(.leading, 10)
.onTapGesture {
onClickNoChatting(userId, nickname, profileUrl)
}
}
if role != .MANAGER && isStaff { if role != .MANAGER && isStaff {
Image("ic_kick_out") Image("ic_kick_out")
.padding(.leading, 10) .padding(.leading, 10)

View File

@ -22,26 +22,27 @@ struct LiveRoomProfilesDialogView: View {
isShowing: Binding<Bool>, isShowing: Binding<Bool>,
viewModel: LiveRoomViewModel, viewModel: LiveRoomViewModel,
roomInfo: GetRoomInfoResponse, roomInfo: GetRoomInfoResponse,
isShowRequestSpeaker: Bool,
onClickRequestSpeaker: @escaping () -> Void,
registerNotification: @escaping () -> Void, registerNotification: @escaping () -> Void,
unRegisterNotification: @escaping () -> Void, unRegisterNotification: @escaping () -> Void,
onClickProfile: @escaping (Int) -> Void onClickProfile: @escaping (Int) -> Void,
onClickNoChatting: @escaping (Int, String, String) -> Void
) { ) {
self._isShowing = isShowing self._isShowing = isShowing
self.viewModel = viewModel self.viewModel = viewModel
self.roomInfo = roomInfo self.roomInfo = roomInfo
self.profiles.append( if !roomInfo.managerList.isEmpty {
AnyView( self.profiles.append(
LiveRoomProfileItemTitleView( AnyView(
title: "스탭", LiveRoomProfileItemTitleView(
count: roomInfo.managerList.count, title: "스탭",
totalCount: nil count: roomInfo.managerList.count,
totalCount: nil
)
.padding(.vertical, 14)
) )
.padding(.vertical, 14)
) )
) }
let isStaff = viewModel.isEqualToStaffId(creatorId: UserDefaults.int(forKey: .userId)) || let isStaff = viewModel.isEqualToStaffId(creatorId: UserDefaults.int(forKey: .userId)) ||
roomInfo.creatorId == UserDefaults.int(forKey: .userId) roomInfo.creatorId == UserDefaults.int(forKey: .userId)
@ -52,28 +53,32 @@ struct LiveRoomProfilesDialogView: View {
LiveRoomProfileItemUserView( LiveRoomProfileItemUserView(
isStaff: isStaff , isStaff: isStaff ,
userId: manager.id, userId: manager.id,
creatorId: roomInfo.creatorId,
nickname: manager.nickname, nickname: manager.nickname,
profileUrl: manager.profileImage, profileUrl: manager.profileImage,
role: manager.role, role: manager.role,
onClickChangeListener: { _ in }, onClickChangeListener: { _ in },
onClickInviteSpeaker: { _ in }, onClickInviteSpeaker: { _ in },
onClickKickOut: { _ in }, onClickKickOut: { _ in },
onClickProfile: onClickProfile onClickProfile: onClickProfile,
onClickNoChatting: { _, _, _ in }
) )
) )
) )
} }
self.profiles.append( if (roomInfo.speakerList.count - 1) > 0 {
AnyView( self.profiles.append(
LiveRoomProfileItemTitleView( AnyView(
title: "스피커", LiveRoomProfileItemTitleView(
count: roomInfo.speakerList.count - 1, title: "스피커",
totalCount: roomInfo.totalAvailableParticipantsCount count: roomInfo.speakerList.count - 1,
totalCount: roomInfo.totalAvailableParticipantsCount
)
.padding(.vertical, 14)
) )
.padding(.vertical, 14)
) )
) }
for speaker in roomInfo.speakerList { for speaker in roomInfo.speakerList {
if speaker.id == roomInfo.creatorId { if speaker.id == roomInfo.creatorId {
@ -94,6 +99,7 @@ struct LiveRoomProfilesDialogView: View {
LiveRoomProfileItemUserView( LiveRoomProfileItemUserView(
isStaff: isStaff, isStaff: isStaff,
userId: speaker.id, userId: speaker.id,
creatorId: roomInfo.creatorId,
nickname: speaker.nickname, nickname: speaker.nickname,
profileUrl: speaker.profileImage, profileUrl: speaker.profileImage,
role: speaker.role, role: speaker.role,
@ -110,23 +116,14 @@ struct LiveRoomProfilesDialogView: View {
viewModel.kickOutId = $0 viewModel.kickOutId = $0
viewModel.isShowKickOutPopup = true viewModel.isShowKickOutPopup = true
}, },
onClickProfile: onClickProfile onClickProfile: onClickProfile,
onClickNoChatting: onClickNoChatting
) )
) )
) )
} }
} }
if isShowRequestSpeaker {
self.profiles.append(
AnyView(
LiveRoomProfileRequestSpeakerView {
onClickRequestSpeaker()
}
)
)
}
self.profiles.append( self.profiles.append(
AnyView( AnyView(
LiveRoomProfileItemTitleView( LiveRoomProfileItemTitleView(
@ -145,6 +142,7 @@ struct LiveRoomProfilesDialogView: View {
LiveRoomProfileItemUserView( LiveRoomProfileItemUserView(
isStaff: isStaff, isStaff: isStaff,
userId: listener.id, userId: listener.id,
creatorId: roomInfo.creatorId,
nickname: listener.nickname, nickname: listener.nickname,
profileUrl: listener.profileImage, profileUrl: listener.profileImage,
role: listener.role, role: listener.role,
@ -163,7 +161,8 @@ struct LiveRoomProfilesDialogView: View {
viewModel.kickOutId = $0 viewModel.kickOutId = $0
viewModel.isShowKickOutPopup = true viewModel.isShowKickOutPopup = true
}, },
onClickProfile: onClickProfile onClickProfile: onClickProfile,
onClickNoChatting: onClickNoChatting
) )
) )
) )

View File

@ -23,6 +23,7 @@ struct LiveRoomUserProfileDialogView: View {
let onClickInviteSpeaker: (Int) -> Void let onClickInviteSpeaker: (Int) -> Void
let onClickChangeListener: (Int) -> Void let onClickChangeListener: (Int) -> Void
let onClickMenu: (Int, String, Bool) -> Void let onClickMenu: (Int, String, Bool) -> Void
let onClickNoChatting: (Int, String, String) -> Void
var body: some View { var body: some View {
ZStack { ZStack {
@ -210,6 +211,22 @@ struct LiveRoomUserProfileDialogView: View {
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.padding(.top, 21.3) .padding(.top, 21.3)
if let _ = userProfile.isManager {
Text("3분간 채팅금지")
.font(.custom(Font.bold.rawValue, size: 15))
.foregroundColor(Color(hex: "9970ff"))
.frame(maxWidth: .infinity)
.padding(.vertical, 13)
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.strokeBorder(lineWidth: 1)
.foregroundColor(Color(hex: "9970ff"))
)
.onTapGesture { onClickNoChatting(userProfile.userId, userProfile.nickname, userProfile.profileUrl) }
.padding(.top, 21.3)
}
Text(userProfile.tags) Text(userProfile.tags)
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "9970ff")) .foregroundColor(Color(hex: "9970ff"))

View File

@ -115,7 +115,7 @@ struct LiveRoomEditView: View {
.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(hex: "eeeeee"))
.accentColor(Color(hex: "9970ff")) .accentColor(Color(hex: "3bb9f1"))
.keyboardType(.default) .keyboardType(.default)
.padding(.top, 12) .padding(.top, 12)
.padding(.horizontal, 6.7) .padding(.horizontal, 6.7)
@ -219,7 +219,7 @@ struct LiveRoomEditView: View {
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.font(.custom(Font.medium.rawValue, size: 14.7)) .font(.custom(Font.medium.rawValue, size: 14.7))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
.accentColor(Color(hex: "9970ff")) .accentColor(Color(hex: "3bb9f1"))
.keyboardType(.numberPad) .keyboardType(.numberPad)
.padding(.vertical, 15.7) .padding(.vertical, 15.7)
.frame(width: screenSize().width - 26.7, alignment: .center) .frame(width: screenSize().width - 26.7, alignment: .center)

View File

@ -8,5 +8,5 @@
import Foundation import Foundation
enum LiveRoomRequestType: String { enum LiveRoomRequestType: String {
case REQUEST_SPEAKER, REQUEST_SPEAKER_ALLOW, INVITE_SPEAKER, CHANGE_LISTENER, KICK_OUT, SET_MANAGER, RELEASE_MANAGER case REQUEST_SPEAKER, REQUEST_SPEAKER_ALLOW, INVITE_SPEAKER, CHANGE_LISTENER, KICK_OUT, SET_MANAGER, RELEASE_MANAGER, NO_CHATTING
} }

View File

@ -25,6 +25,7 @@ struct LiveRoomTopCreatorView: View {
.scaledToFill() .scaledToFill()
.frame(width: 33.3, height: 33.3) .frame(width: 33.3, height: 33.3)
.clipShape(Circle()) .clipShape(Circle())
.contentShape(Circle())
.onTapGesture { onClickProfile() } .onTapGesture { onClickProfile() }
Image("ic_crown") Image("ic_crown")
@ -33,11 +34,11 @@ struct LiveRoomTopCreatorView: View {
.font(.custom(Font.light.rawValue, size: 12)) .font(.custom(Font.light.rawValue, size: 12))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color(hex: "eeeeee"))
Spacer()
if creatorId != UserDefaults.int(forKey: .userId) { if creatorId != UserDefaults.int(forKey: .userId) {
Image(isFollowing ? "btn_following" : "btn_follow") Image(isFollowing ? "btn_following" : "btn_follow")
.contentShape(Rectangle())
.onTapGesture { onClickFollow(isFollowing) } .onTapGesture { onClickFollow(isFollowing) }
.padding(.leading, 13.3)
} }
} }
} }

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