Compare commits
17 Commits
afca24c221
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d369bc11f7 | ||
|
|
f542191d46 | ||
|
|
1d120b58bd | ||
|
|
44daabdcae | ||
|
|
0844c6f4d7 | ||
|
|
c6a6b3c79e | ||
|
|
4f66ffb595 | ||
|
|
91b5ed974f | ||
|
|
af31444f0f | ||
|
|
8eca5df62b | ||
|
|
793b5dd95a | ||
|
|
70003af82b | ||
|
|
0a22f87acc | ||
|
|
8b102905ad | ||
|
|
e9fc7e180d | ||
|
|
84d8e2f2e3 | ||
|
|
b51d643db8 |
21
SodaLive/Resources/Assets.xcassets/ic_ice.imageset/Contents.json
vendored
Normal file
21
SodaLive/Resources/Assets.xcassets/ic_ice.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "ic_ice.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
SodaLive/Resources/Assets.xcassets/ic_ice.imageset/ic_ice.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/ic_ice.imageset/ic_ice.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -1276,6 +1276,7 @@
|
||||
}
|
||||
},
|
||||
"※ 인기 순위는 매주 업데이트됩니다." : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -1324,6 +1325,7 @@
|
||||
}
|
||||
},
|
||||
"※ 최근 2주간 등록된 새로운 ASMR 입니다." : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -1340,6 +1342,7 @@
|
||||
}
|
||||
},
|
||||
"※ 최근 2주간 등록된 새로운 라이브 다시듣기 입니다." : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -1356,6 +1359,7 @@
|
||||
}
|
||||
},
|
||||
"※ 최근 2주간 등록된 새로운 알람 입니다." : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -3938,6 +3942,7 @@
|
||||
}
|
||||
},
|
||||
"마이페이지에서 본인인증을 해주세요" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -4129,22 +4134,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"모집완료" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Recruitment closed"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "募集終了"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"목" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -4179,6 +4168,22 @@
|
||||
},
|
||||
"모서리 원을 드래그해서 크롭 영역 크기를 조정하세요" : {
|
||||
|
||||
},
|
||||
"모집완료" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Recruitment closed"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "募集終了"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"모집중" : {
|
||||
"localizations" : {
|
||||
@@ -4597,6 +4602,7 @@
|
||||
}
|
||||
},
|
||||
"보이스 모닝콜" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -5173,6 +5179,7 @@
|
||||
}
|
||||
},
|
||||
"새로운 콘텐츠" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -5384,6 +5391,7 @@
|
||||
}
|
||||
},
|
||||
"숏플" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -6459,6 +6467,7 @@
|
||||
}
|
||||
},
|
||||
"오리지널 오디오 드라마" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -7051,6 +7060,7 @@
|
||||
}
|
||||
},
|
||||
"인기 시리즈" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -7179,6 +7189,7 @@
|
||||
}
|
||||
},
|
||||
"일간 랭킹" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -7339,6 +7350,7 @@
|
||||
}
|
||||
},
|
||||
"자세히 >" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -7515,6 +7527,7 @@
|
||||
}
|
||||
},
|
||||
"장르별 추천 시리즈" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -8801,6 +8814,7 @@
|
||||
}
|
||||
},
|
||||
"콘텐츠 마켓" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -9121,6 +9135,7 @@
|
||||
}
|
||||
},
|
||||
"태그별 추천 콘텐츠" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
|
||||
@@ -74,6 +74,7 @@ class AppState: ObservableObject {
|
||||
|
||||
@Published var isShowErrorPopup = false
|
||||
@Published var errorMessage = ""
|
||||
@Published private var pendingContentSettingsGuideMessage: String? = nil
|
||||
@Published var liveDetailSheet: LiveDetailSheetState? = nil
|
||||
|
||||
private func syncStepWithNavigationPath() {
|
||||
@@ -180,6 +181,16 @@ class AppState: ObservableObject {
|
||||
pendingCommunityCommentCreatorId = 0
|
||||
pendingCommunityCommentPostId = 0
|
||||
}
|
||||
|
||||
func setPendingContentSettingsGuideMessage(_ message: String) {
|
||||
pendingContentSettingsGuideMessage = message
|
||||
}
|
||||
|
||||
func consumePendingContentSettingsGuideMessage() -> String? {
|
||||
let message = pendingContentSettingsGuideMessage
|
||||
pendingContentSettingsGuideMessage = nil
|
||||
return message
|
||||
}
|
||||
|
||||
// 언어 적용 직후 앱을 소프트 재시작(스플래시 -> 메인)하여 전역 UI를 새로고침
|
||||
func softRestart() {
|
||||
|
||||
@@ -150,18 +150,6 @@ enum AppStep {
|
||||
|
||||
case search
|
||||
|
||||
case contentMain(startTab: ContentMainTab)
|
||||
|
||||
case completedSeriesAll
|
||||
|
||||
case newAlarmContentAll
|
||||
|
||||
case newAsmrContentAll
|
||||
|
||||
case newReplayContentAll
|
||||
|
||||
case introduceCreatorAll
|
||||
|
||||
case message
|
||||
|
||||
case notificationList
|
||||
|
||||
@@ -75,6 +75,9 @@ final class AppViewModel: ObservableObject {
|
||||
UserDefaults.set(data.isAuth, forKey: .auth)
|
||||
UserDefaults.set(data.role.rawValue, forKey: .role)
|
||||
UserDefaults.set(data.auditionNotice ?? false, forKey: .isAuditionNotification)
|
||||
UserDefaults.set(data.countryCode, forKey: .countryCode)
|
||||
UserDefaults.set(data.isAdultContentVisible, forKey: .isAdultContentVisible)
|
||||
UserDefaults.set(data.contentType, forKey: .contentPreference)
|
||||
if data.followingChannelLiveNotice == nil && data.followingChannelUploadContentNotice == nil && data.messageNotice == nil {
|
||||
AppState.shared.isShowNotificationSettingsDialog = true
|
||||
}
|
||||
|
||||
@@ -41,7 +41,14 @@ struct ChatTabView: View {
|
||||
AppState.shared.setAppStep(step: .login)
|
||||
return
|
||||
}
|
||||
if auth == false {
|
||||
|
||||
let normalizedCountryCode = UserDefaults
|
||||
.string(forKey: .countryCode)
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.uppercased()
|
||||
let isKoreanCountry = normalizedCountryCode.isEmpty || normalizedCountryCode == "KR"
|
||||
|
||||
if isKoreanCountry && auth == false {
|
||||
pendingAction = {
|
||||
AppState.shared
|
||||
.setAppStep(step: .characterDetail(characterId: characterId))
|
||||
@@ -49,8 +56,20 @@ struct ChatTabView: View {
|
||||
isShowAuthConfirmView = true
|
||||
return
|
||||
}
|
||||
|
||||
if !UserDefaults.isAdultContentVisible() {
|
||||
pendingAction = nil
|
||||
moveToContentSettingsWithGuideToast()
|
||||
return
|
||||
}
|
||||
|
||||
AppState.shared.setAppStep(step: .characterDetail(characterId: characterId))
|
||||
}
|
||||
|
||||
private func moveToContentSettingsWithGuideToast() {
|
||||
AppState.shared.setPendingContentSettingsGuideMessage(I18n.Settings.adultContentEnableGuide)
|
||||
AppState.shared.setAppStep(step: .contentViewSettings)
|
||||
}
|
||||
|
||||
private func handleCharacterSelection() {
|
||||
let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
@@ -9,7 +9,7 @@ import Foundation
|
||||
import Moya
|
||||
|
||||
enum ContentApi {
|
||||
case getAudioContentList(userId: Int, categoryId: Int, isAdultContentVisible: Bool, page: Int, size: Int, sort: ContentListViewModel.Sort)
|
||||
case getAudioContentList(userId: Int, categoryId: Int, page: Int, size: Int, sort: ContentListViewModel.Sort)
|
||||
case getAudioContentDetail(audioContentId: Int)
|
||||
case likeContent(request: PutAudioContentLikeRequest)
|
||||
case registerComment(request: RegisterAudioContentCommentRequest)
|
||||
@@ -25,51 +25,51 @@ enum ContentApi {
|
||||
case getNewContentUploadCreatorList
|
||||
case getMainBannerList
|
||||
case getMainOrderList
|
||||
case getNewContentOfTheme(theme: String, isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getCurationList(isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int)
|
||||
case getNewContentOfTheme(theme: String)
|
||||
case getCurationList(page: Int, size: Int)
|
||||
case donation(request: AudioContentDonationRequest)
|
||||
case modifyComment(request: ModifyCommentRequest)
|
||||
case getNewContentThemeList(isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getNewContentAllOfTheme(isFree: Bool, theme: String, isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int)
|
||||
case getAudioContentListByCurationId(curationId: Int, isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int, sort: ContentCurationViewModel.Sort)
|
||||
case getContentRanking(isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int, sortType: String)
|
||||
case getNewContentThemeList
|
||||
case getNewContentAllOfTheme(isFree: Bool, theme: String, page: Int, size: Int)
|
||||
case getAudioContentListByCurationId(curationId: Int, page: Int, size: Int, sort: ContentCurationViewModel.Sort)
|
||||
case getContentRanking(page: Int, size: Int, sortType: String)
|
||||
case getContentRankingSortType
|
||||
case pinContent(contentId: Int)
|
||||
case unpinContent(contentId: Int)
|
||||
case getAudioContentByTheme(themeId: Int, isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int, sort: ContentAllByThemeViewModel.Sort)
|
||||
case getAudioContentByTheme(themeId: Int, page: Int, size: Int, sort: ContentAllByThemeViewModel.Sort)
|
||||
case generateUrl(contentId: Int)
|
||||
|
||||
case getContentMainHome(isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getPopularContentByCreator(creatorId: Int, isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getContentMainHomeContentRanking(isAdultContentVisible: Bool, contentType: ContentType, sortType: String)
|
||||
case getContentMainHome
|
||||
case getPopularContentByCreator(creatorId: Int)
|
||||
case getContentMainHomeContentRanking(sortType: String)
|
||||
|
||||
case getContentMainSeries(isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getRecommendSeriesListByGenre(genreId: Int, isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getRecommendSeriesByCreator(creatorId: Int, isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getCompletedSeries(isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int)
|
||||
case getContentMainSeries
|
||||
case getRecommendSeriesListByGenre(genreId: Int)
|
||||
case getRecommendSeriesByCreator(creatorId: Int)
|
||||
case getCompletedSeries(page: Int, size: Int)
|
||||
|
||||
case getContentMainContent(isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getContentMainNewContentOfTheme(theme: String, isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getDailyContentRanking(sortType: String, isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getRecommendContentByTag(tag: String, contentType: ContentType)
|
||||
case getContentMainContentPopularContentByCreator(creatorId: Int, isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getContentMainContent
|
||||
case getContentMainNewContentOfTheme(theme: String)
|
||||
case getDailyContentRanking(sortType: String)
|
||||
case getRecommendContentByTag(tag: String)
|
||||
case getContentMainContentPopularContentByCreator(creatorId: Int)
|
||||
|
||||
case getContentMainAlarm(isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getContentMainAlarmAll(theme: String, isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int)
|
||||
case getContentMainAlarm
|
||||
case getContentMainAlarmAll(theme: String, page: Int, size: Int)
|
||||
|
||||
case getContentMainAsmr(isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getPopularAsmrContentByCreator(creatorId: Int, isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getContentMainAsmr
|
||||
case getPopularAsmrContentByCreator(creatorId: Int)
|
||||
|
||||
case getContentMainReplay(isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getPopularReplayContentByCreator(creatorId: Int, isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getContentMainReplay
|
||||
case getPopularReplayContentByCreator(creatorId: Int)
|
||||
|
||||
case getContentMainFree(isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getIntroduceCreatorList(isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int)
|
||||
case getNewFreeContentOfTheme(theme: String, isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int)
|
||||
case getPopularFreeContentByCreator(creatorId: Int, isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getContentMainFree
|
||||
case getIntroduceCreatorList(page: Int, size: Int)
|
||||
case getNewFreeContentOfTheme(theme: String, page: Int, size: Int)
|
||||
case getPopularFreeContentByCreator(creatorId: Int)
|
||||
|
||||
case getAllAudioContents(isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int, isFree: Bool?, isPointAvailableOnly: Bool?, sortType: ContentAllViewModel.Sort = .NEWEST, theme: String? = nil)
|
||||
case getAudioContentActiveThemeList(isAdultContentVisible: Bool, contentType: ContentType, isFree: Bool?, isPointAvailableOnly: Bool?)
|
||||
case getAllAudioContents(page: Int, size: Int, isFree: Bool?, isPointAvailableOnly: Bool?, sortType: ContentAllViewModel.Sort = .NEWEST, theme: String? = nil)
|
||||
case getAudioContentActiveThemeList(isFree: Bool?, isPointAvailableOnly: Bool?)
|
||||
}
|
||||
|
||||
extension ContentApi: TargetType {
|
||||
@@ -145,7 +145,7 @@ extension ContentApi: TargetType {
|
||||
case .getNewContentAllOfTheme:
|
||||
return "/audio-content/main/new/all"
|
||||
|
||||
case .getAudioContentListByCurationId(let curationId, _, _, _, _, _):
|
||||
case .getAudioContentListByCurationId(let curationId, _, _, _):
|
||||
return "/audio-content/curation/\(curationId)"
|
||||
|
||||
case .getContentRanking:
|
||||
@@ -160,7 +160,7 @@ extension ContentApi: TargetType {
|
||||
case .unpinContent(let contentId):
|
||||
return "/audio-content/unpin-at-the-top/\(contentId)"
|
||||
|
||||
case .getAudioContentByTheme(let themeId, _, _, _, _, _):
|
||||
case .getAudioContentByTheme(let themeId, _, _, _):
|
||||
return "/audio-content/theme/\(themeId)/content"
|
||||
|
||||
case .generateUrl(let contentId):
|
||||
@@ -273,11 +273,10 @@ extension ContentApi: TargetType {
|
||||
|
||||
var task: Moya.Task {
|
||||
switch self {
|
||||
case .getAudioContentList(let userId, let categoryId, let isAdultContentVisible, let page, let size, let sort):
|
||||
case .getAudioContentList(let userId, let categoryId, let page, let size, let sort):
|
||||
let parameters = [
|
||||
"creator-id": userId,
|
||||
"category-id": categoryId,
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"page": page - 1,
|
||||
"size": size,
|
||||
"sort-type": sort
|
||||
@@ -339,11 +338,9 @@ extension ContentApi: TargetType {
|
||||
case .deleteAudioContent:
|
||||
return .requestPlain
|
||||
|
||||
case .getNewContentOfTheme(let theme, let isAdultContentVisible, let contentType):
|
||||
case .getNewContentOfTheme(let theme):
|
||||
let parameters = [
|
||||
"theme": theme,
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType
|
||||
"theme": theme
|
||||
] as [String : Any]
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
@@ -354,30 +351,21 @@ extension ContentApi: TargetType {
|
||||
case .modifyComment(let request):
|
||||
return .requestJSONEncodable(request)
|
||||
|
||||
case .getNewContentThemeList(let isAdultContentVisible, let contentType):
|
||||
let parameters = [
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType
|
||||
] as [String : Any]
|
||||
case .getNewContentThemeList:
|
||||
return .requestPlain
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getNewContentAllOfTheme(let isFree, let theme, let isAdultContentVisible, let contentType, let page, let size):
|
||||
case .getNewContentAllOfTheme(let isFree, let theme, let page, let size):
|
||||
let parameters = [
|
||||
"isFree": isFree,
|
||||
"theme": theme,
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType,
|
||||
"page": page - 1,
|
||||
"size": size
|
||||
] as [String : Any]
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getAudioContentListByCurationId(_, let isAdultContentVisible, let contentType, let page, let size, let sort):
|
||||
case .getAudioContentListByCurationId(_, let page, let size, let sort):
|
||||
let parameters = [
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType,
|
||||
"page": page - 1,
|
||||
"size": size,
|
||||
"sort-type": sort
|
||||
@@ -385,10 +373,8 @@ extension ContentApi: TargetType {
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getContentRanking(let isAdultContentVisible, let contentType, let page, let size, let sortType):
|
||||
case .getContentRanking(let page, let size, let sortType):
|
||||
let parameters = [
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType,
|
||||
"page": page - 1,
|
||||
"size": size,
|
||||
"sort-type": sortType
|
||||
@@ -399,10 +385,8 @@ extension ContentApi: TargetType {
|
||||
case .getContentRankingSortType:
|
||||
return .requestPlain
|
||||
|
||||
case .getCurationList(let isAdultContentVisible, let contentType, let page, let size):
|
||||
case .getCurationList(let page, let size):
|
||||
let parameters = [
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType,
|
||||
"page": page - 1,
|
||||
"size": size
|
||||
] as [String : Any]
|
||||
@@ -412,10 +396,8 @@ extension ContentApi: TargetType {
|
||||
case .pinContent, .unpinContent:
|
||||
return .requestPlain
|
||||
|
||||
case .getAudioContentByTheme(_, let isAdultContentVisible, let contentType, let page, let size, let sort):
|
||||
case .getAudioContentByTheme(_, let page, let size, let sort):
|
||||
let parameters = [
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType,
|
||||
"page": page - 1,
|
||||
"size": size,
|
||||
"sort-type": sort
|
||||
@@ -423,141 +405,109 @@ extension ContentApi: TargetType {
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getContentMainHome(let isAdultContentVisible, let contentType),
|
||||
.getContentMainSeries(let isAdultContentVisible, let contentType),
|
||||
.getContentMainContent(let isAdultContentVisible, let contentType),
|
||||
.getContentMainAlarm(let isAdultContentVisible, let contentType),
|
||||
.getContentMainAsmr(let isAdultContentVisible, let contentType),
|
||||
.getContentMainReplay(let isAdultContentVisible, let contentType),
|
||||
.getContentMainFree(let isAdultContentVisible, let contentType):
|
||||
case .getContentMainHome,
|
||||
.getContentMainSeries,
|
||||
.getContentMainContent,
|
||||
.getContentMainAlarm,
|
||||
.getContentMainAsmr,
|
||||
.getContentMainReplay,
|
||||
.getContentMainFree:
|
||||
return .requestPlain
|
||||
|
||||
case .getRecommendSeriesListByGenre(let genreId):
|
||||
let parameters = [
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType
|
||||
"genreId": genreId
|
||||
] as [String : Any]
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getRecommendSeriesListByGenre(let genreId, let isAdultContentVisible, let contentType):
|
||||
case .getPopularContentByCreator(let creatorId):
|
||||
let parameters = [
|
||||
"genreId": genreId,
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType
|
||||
"creatorId": creatorId
|
||||
] as [String : Any]
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getPopularContentByCreator(let creatorId, let isAdultContentVisible, let contentType):
|
||||
case .getContentMainHomeContentRanking(let sortType):
|
||||
let parameters = [
|
||||
"creatorId": creatorId,
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType
|
||||
"sort-type": sortType
|
||||
] as [String : Any]
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getContentMainHomeContentRanking(let isAdultContentVisible, let contentType, let sortType):
|
||||
case .getRecommendSeriesByCreator(let creatorId):
|
||||
let parameters = [
|
||||
"sort-type": sortType,
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType
|
||||
"creatorId": creatorId
|
||||
] as [String : Any]
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getRecommendSeriesByCreator(let creatorId, let isAdultContentVisible, let contentType):
|
||||
case .getContentMainNewContentOfTheme(let theme):
|
||||
let parameters = [
|
||||
"creatorId": creatorId,
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType
|
||||
"theme": theme
|
||||
] as [String : Any]
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getDailyContentRanking(let sortType):
|
||||
let parameters = [
|
||||
"sort-type": sortType
|
||||
] as [String : Any]
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getRecommendContentByTag(let tag):
|
||||
let parameters = [
|
||||
"tag": tag
|
||||
] as [String : Any]
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getContentMainContentPopularContentByCreator(let creatorId):
|
||||
let parameters = [
|
||||
"creatorId": creatorId
|
||||
] as [String : Any]
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getContentMainNewContentOfTheme(let theme, let isAdultContentVisible, let contentType):
|
||||
case .getNewFreeContentOfTheme(let theme, let page, let size):
|
||||
let parameters = [
|
||||
"theme": theme,
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType
|
||||
"page": page - 1,
|
||||
"size": size
|
||||
] as [String : Any]
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getDailyContentRanking(let sortType, let isAdultContentVisible, let contentType):
|
||||
let parameters = [
|
||||
"sort-type": sortType,
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType
|
||||
] as [String : Any]
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getRecommendContentByTag(let tag, let contentType):
|
||||
let parameters = [
|
||||
"tag": tag,
|
||||
"contentType": contentType
|
||||
] as [String : Any]
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getContentMainContentPopularContentByCreator(let creatorId, let isAdultContentVisible, let contentType):
|
||||
let parameters = [
|
||||
"creatorId": creatorId,
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType
|
||||
] as [String : Any]
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getNewFreeContentOfTheme(let theme, let isAdultContentVisible, let contentType, let page, let size):
|
||||
case .getContentMainAlarmAll(let theme, let page, let size):
|
||||
let parameters = [
|
||||
"theme": theme,
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType,
|
||||
"page": page - 1,
|
||||
"size": size
|
||||
] as [String : Any]
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getContentMainAlarmAll(let theme, let isAdultContentVisible, let contentType, let page, let size):
|
||||
case .getIntroduceCreatorList(let page, let size):
|
||||
let parameters = [
|
||||
"theme": theme,
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType,
|
||||
"page": page - 1,
|
||||
"size": size
|
||||
] as [String : Any]
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getIntroduceCreatorList(let isAdultContentVisible, let contentType, let page, let size):
|
||||
case .getPopularAsmrContentByCreator(let creatorId),
|
||||
.getPopularReplayContentByCreator(let creatorId),
|
||||
.getPopularFreeContentByCreator(let creatorId):
|
||||
let parameters = [
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType,
|
||||
"page": page - 1,
|
||||
"size": size
|
||||
"creatorId": creatorId
|
||||
] as [String : Any]
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getPopularAsmrContentByCreator(let creatorId, let isAdultContentVisible, let contentType),
|
||||
.getPopularReplayContentByCreator(let creatorId, let isAdultContentVisible, let contentType),
|
||||
.getPopularFreeContentByCreator(let creatorId, let isAdultContentVisible, let contentType):
|
||||
case .getCompletedSeries(let page, let size):
|
||||
let parameters = [
|
||||
"creatorId": creatorId,
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType
|
||||
] as [String : Any]
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getCompletedSeries(let isAdultContentVisible, let contentType, let page, let size):
|
||||
let parameters = [
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType,
|
||||
"page": page - 1,
|
||||
"size": size
|
||||
] as [String : Any]
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getAllAudioContents(let isAdultContentVisible, let contentType, let page, let size, let isFree, let isPointAvailableOnly, let sortType, let theme):
|
||||
case .getAllAudioContents(let page, let size, let isFree, let isPointAvailableOnly, let sortType, let theme):
|
||||
var parameters = [
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType,
|
||||
"sort-type": sortType,
|
||||
"page": page - 1,
|
||||
"size": size
|
||||
@@ -577,11 +527,8 @@ extension ContentApi: TargetType {
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getAudioContentActiveThemeList(let isAdultContentVisible, let contentType, let isFree, let isPointAvailableOnly):
|
||||
var parameters = [
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType,
|
||||
] as [String : Any]
|
||||
case .getAudioContentActiveThemeList(let isFree, let isPointAvailableOnly):
|
||||
var parameters = [String : Any]()
|
||||
|
||||
if let isFree = isFree {
|
||||
parameters["isFree"] = isFree
|
||||
|
||||
@@ -20,7 +20,6 @@ final class ContentRepository {
|
||||
.getAudioContentList(
|
||||
userId: userId,
|
||||
categoryId: categoryId,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
page: page,
|
||||
size: size,
|
||||
sort: sort)
|
||||
@@ -89,19 +88,13 @@ final class ContentRepository {
|
||||
|
||||
func getNewContentOfTheme(theme: String) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getNewContentOfTheme(
|
||||
theme: theme,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
.getNewContentOfTheme(theme: theme)
|
||||
)
|
||||
}
|
||||
|
||||
func getCurationList(page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getCurationList(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
|
||||
page: page,
|
||||
size: size
|
||||
)
|
||||
@@ -117,12 +110,7 @@ final class ContentRepository {
|
||||
}
|
||||
|
||||
func getNewContentThemeList() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getNewContentThemeList(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
return api.requestPublisher(.getNewContentThemeList)
|
||||
}
|
||||
|
||||
func getNewContentAllOfTheme(isFree: Bool, theme: String, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
@@ -130,8 +118,6 @@ final class ContentRepository {
|
||||
.getNewContentAllOfTheme(
|
||||
isFree: isFree,
|
||||
theme: theme,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
|
||||
page: page,
|
||||
size: size
|
||||
)
|
||||
@@ -142,8 +128,6 @@ final class ContentRepository {
|
||||
return api.requestPublisher(
|
||||
.getAudioContentListByCurationId(
|
||||
curationId: curationId,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
|
||||
page: page,
|
||||
size: size,
|
||||
sort: sort
|
||||
@@ -158,8 +142,6 @@ final class ContentRepository {
|
||||
func getContentRanking(page: Int, size: Int, sortType: String = "매출") -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getContentRanking(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
|
||||
page: page,
|
||||
size: size,
|
||||
sortType: sortType
|
||||
@@ -183,8 +165,6 @@ final class ContentRepository {
|
||||
return api.requestPublisher(
|
||||
.getAudioContentByTheme(
|
||||
themeId: themeId,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
|
||||
page: page,
|
||||
size: size,
|
||||
sort: sort
|
||||
@@ -206,8 +186,6 @@ final class ContentRepository {
|
||||
) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getAllAudioContents(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
|
||||
page: page,
|
||||
size: size,
|
||||
isFree: isFree,
|
||||
@@ -221,8 +199,6 @@ final class ContentRepository {
|
||||
func getAudioContentActiveThemeList(isFree: Bool, isPointAvailableOnly: Bool) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getAudioContentActiveThemeList(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
|
||||
isFree: isFree,
|
||||
isPointAvailableOnly: isPointAvailableOnly
|
||||
)
|
||||
|
||||
@@ -24,6 +24,14 @@ struct ContentCreateView: View {
|
||||
@State private var isShowSelectTimeView = false
|
||||
|
||||
var body: some View {
|
||||
let normalizedCountryCode = UserDefaults
|
||||
.string(forKey: .countryCode)
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.uppercased()
|
||||
let isKoreanCountry = normalizedCountryCode.isEmpty || normalizedCountryCode == "KR"
|
||||
let isAdultContentVisible = UserDefaults.isAdultContentVisible()
|
||||
let shouldShowAdultSetting = isAdultContentVisible && (!isKoreanCountry || UserDefaults.bool(forKey: .auth))
|
||||
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
GeometryReader { proxy in
|
||||
ZStack {
|
||||
@@ -448,34 +456,36 @@ struct ContentCreateView: View {
|
||||
.padding(.top, 26.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
VStack(spacing: 13.3) {
|
||||
Text("연령 제한")
|
||||
.appFont(size: 16.7, weight: .bold)
|
||||
.foregroundColor(Color.grayee)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
HStack(spacing: 13.3) {
|
||||
SelectButtonView(title: I18n.CreateContent.allAges, isChecked: !viewModel.isAdult) {
|
||||
if viewModel.isAdult {
|
||||
viewModel.isAdult = false
|
||||
}
|
||||
}
|
||||
|
||||
SelectButtonView(title: I18n.CreateContent.over19, isChecked: viewModel.isAdult) {
|
||||
if !viewModel.isAdult {
|
||||
viewModel.isAdult = true
|
||||
if shouldShowAdultSetting {
|
||||
VStack(spacing: 13.3) {
|
||||
Text("연령 제한")
|
||||
.appFont(size: 16.7, weight: .bold)
|
||||
.foregroundColor(Color.grayee)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
HStack(spacing: 13.3) {
|
||||
SelectButtonView(title: I18n.CreateContent.allAges, isChecked: !viewModel.isAdult) {
|
||||
if viewModel.isAdult {
|
||||
viewModel.isAdult = false
|
||||
}
|
||||
}
|
||||
|
||||
SelectButtonView(title: I18n.CreateContent.over19, isChecked: viewModel.isAdult) {
|
||||
if !viewModel.isAdult {
|
||||
viewModel.isAdult = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text("성인콘텐츠를 전체관람가로 등록할 시 발생하는 법적 책임은 회사와 상관없이 콘텐츠를 등록한 본인에게 있습니다.\n콘텐츠 내용은 물론 제목도 19금 여부를 체크해 주시기 바랍니다.")
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(Color.mainRed3)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.top, 13.3)
|
||||
}
|
||||
|
||||
Text("성인콘텐츠를 전체관람가로 등록할 시 발생하는 법적 책임은 회사와 상관없이 콘텐츠를 등록한 본인에게 있습니다.\n콘텐츠 내용은 물론 제목도 19금 여부를 체크해 주시기 바랍니다.")
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(Color.mainRed3)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.top, 13.3)
|
||||
.padding(.top, 26.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
.padding(.top, 26.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
VStack(spacing: 13.3) {
|
||||
Text("댓글 가능 여부")
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
//
|
||||
// ContentMainView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/09.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainView: View {
|
||||
|
||||
@StateObject var viewModel = ContentMainViewModel()
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
Color.black.ignoresSafeArea()
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
Text("콘텐츠 마켓")
|
||||
.appFont(size: 21.3, weight: .bold)
|
||||
.foregroundColor(Color.button)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("ic_content_keep")
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(step: .myBox(currentTab: .orderlist))
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 26.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
if !viewModel.isLoading {
|
||||
ContentMainBannerView()
|
||||
.padding(.bottom, 26.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Image("ic_title_search_black")
|
||||
|
||||
Text("채널명을 입력해 보세요")
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(Color.gray55)
|
||||
.keyboardType(.default)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 21.3)
|
||||
.frame(height: 50)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.gray22)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 6.7)
|
||||
.strokeBorder(lineWidth: 1)
|
||||
.foregroundColor(Color.graybb)
|
||||
)
|
||||
.padding(.bottom, 40)
|
||||
.padding(.horizontal, 13.3)
|
||||
.onTapGesture {
|
||||
UserDefaults.set("", forKey: .searchChannel)
|
||||
AppState.shared.setAppStep(step: .search)
|
||||
}
|
||||
|
||||
ContentMainCreatorRankingView()
|
||||
.padding(.bottom, 40)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ContentMainRecommendSeriesView()
|
||||
|
||||
HStack(spacing: 8) {
|
||||
ZStack {
|
||||
Image("img_bg_morning_call")
|
||||
.resizable()
|
||||
.frame(height: 53.3)
|
||||
.frame(maxWidth: .infinity)
|
||||
.cornerRadius(2.6)
|
||||
|
||||
HStack(spacing: 2.7) {
|
||||
Image("ic_alarm_clock_blue")
|
||||
|
||||
Text("보이스 모닝콜")
|
||||
.appFont(size: 16.7, weight: .bold)
|
||||
.foregroundColor(Color(hex: "0057ff"))
|
||||
}
|
||||
.cornerRadius(2.6)
|
||||
}
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(
|
||||
step: .contentAllByTheme(themeId: 12)
|
||||
)
|
||||
}
|
||||
|
||||
ZStack {
|
||||
Image("img_bg_short_play")
|
||||
.resizable()
|
||||
.frame(height: 53.3)
|
||||
.frame(maxWidth: .infinity)
|
||||
.cornerRadius(2.6)
|
||||
|
||||
HStack(spacing: 2.7) {
|
||||
Image("ic_short_play")
|
||||
|
||||
Text("숏플")
|
||||
.appFont(size: 16.7, weight: .bold)
|
||||
.foregroundColor(Color(hex: "dd158d"))
|
||||
}
|
||||
.cornerRadius(2.6)
|
||||
}
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(
|
||||
step: .contentAllByTheme(themeId: 11)
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 40)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ContentMainNewContentView()
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ContentMainRankingView()
|
||||
.padding(.top, 40)
|
||||
.padding(.horizontal, 13.3)
|
||||
.animation(nil)
|
||||
|
||||
ContentMainCurationView()
|
||||
.padding(.top, 40)
|
||||
.padding(.bottom, 20)
|
||||
|
||||
Text("""
|
||||
- 회사명 : 주식회사 소다라이브
|
||||
|
||||
- 대표자 : 이재형
|
||||
|
||||
- 주소 : 경기도 성남시 분당구 황새울로335번길 10, 5층 563A호
|
||||
|
||||
- 사업자등록번호 : 870-81-03220
|
||||
|
||||
- 통신판매업신고 : 제2024-성남분당B-1012호
|
||||
|
||||
- 고객센터 : 02.2055.1477 (이용시간 10:00~19:00)
|
||||
|
||||
- 대표 이메일 : sodalive.official@gmail.com
|
||||
""")
|
||||
.appFont(size: 11, weight: .medium)
|
||||
.foregroundColor(Color.gray77)
|
||||
.padding(.top, 13.3)
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 13.3)
|
||||
}
|
||||
|
||||
if UserDefaults.string(forKey: .role) == MemberRole.CREATOR.rawValue {
|
||||
HStack(spacing: 5) {
|
||||
Image("ic_thumb_play")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
|
||||
Text("콘텐츠 업로드")
|
||||
.appFont(size: 13.3, weight: .bold)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(13.3)
|
||||
.background(Color(hex: "3bb9f1"))
|
||||
.cornerRadius(44)
|
||||
.padding(.trailing, 16.7)
|
||||
.padding(.bottom, 16.7)
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(step: .createContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if viewModel.isLoading {
|
||||
LoadingView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentMainView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentMainView()
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
//
|
||||
// ContentMainViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/11.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import Combine
|
||||
|
||||
final class ContentMainViewModel: ObservableObject {
|
||||
@Published var isLoading = false
|
||||
|
||||
func refresh() {
|
||||
isLoading = true
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [unowned self] in
|
||||
self.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
//
|
||||
// ContentMainCurationItemView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/11.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainCurationItemView: View {
|
||||
|
||||
let item: GetAudioContentCurationResponse
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
Text(item.title)
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.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)
|
||||
.appFont(size: 13, weight: .medium)
|
||||
.foregroundColor(Color(hex: "777777"))
|
||||
.padding(.top, 4)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack(alignment: .top, spacing: 13.3) {
|
||||
ForEach(0..<item.contents.count, id: \.self) {
|
||||
let audioContent = item.contents[$0]
|
||||
ContentMainItemView(item: audioContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 13.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
//
|
||||
// ContentMainCurationView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/11.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainCurationView: View {
|
||||
|
||||
@StateObject private var viewModel = ContentMainCurationViewModel()
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if !viewModel.curationList.isEmpty {
|
||||
LazyVStack(spacing: 40) {
|
||||
ForEach(0..<viewModel.curationList.count, id: \.self) { index in
|
||||
ContentMainCurationItemView(item: viewModel.curationList[index])
|
||||
.padding(.horizontal, 13.3)
|
||||
.onAppear {
|
||||
if index == viewModel.curationList.count - 1 {
|
||||
viewModel.getCurationList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if viewModel.isLoading {
|
||||
ActivityIndicatorView()
|
||||
.frame(width: 100, height: 100)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.onAppear {
|
||||
viewModel.page = 1
|
||||
viewModel.isLast = false
|
||||
viewModel.getCurationList()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
//
|
||||
// ContentMainCurationViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/12/11.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class ContentMainCurationViewModel: ObservableObject {
|
||||
private let repository = ContentRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var isLoading = false
|
||||
|
||||
@Published var curationList = [GetAudioContentCurationResponse]()
|
||||
|
||||
var isLast = false
|
||||
var page = 1
|
||||
private let size = 10
|
||||
|
||||
func getCurationList() {
|
||||
if !isLoading && !isLast {
|
||||
isLoading = true
|
||||
|
||||
repository.getCurationList(page: page, size: size)
|
||||
.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<[GetAudioContentCurationResponse]>.self, from: responseData)
|
||||
self.isLoading = false
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
if page == 1 {
|
||||
self.curationList.removeAll()
|
||||
}
|
||||
|
||||
if !data.isEmpty {
|
||||
page += 1
|
||||
self.curationList.append(contentsOf: data)
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,17 +7,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GetAudioContentMainResponse: Decodable {
|
||||
let newContentUploadCreatorList: [ContentCreatorResponse]
|
||||
let bannerList: [GetAudioContentBannerResponse]
|
||||
let orderList: [GetAudioContentMainItem]
|
||||
let themeList: [String]
|
||||
let newContentList: [GetAudioContentMainItem]
|
||||
let curationList: [GetAudioContentCurationResponse]
|
||||
let contentRankingSortTypeList: [String]
|
||||
let contentRanking: GetAudioContentRanking
|
||||
}
|
||||
|
||||
struct GetAudioContentRanking: Decodable {
|
||||
let startDate: String
|
||||
let endDate: String
|
||||
@@ -37,12 +26,6 @@ struct GetAudioContentRankingItem: Decodable {
|
||||
let creatorProfileImageUrl: String
|
||||
}
|
||||
|
||||
struct ContentCreatorResponse: Decodable {
|
||||
let creatorId: Int
|
||||
let creatorNickname: String
|
||||
let creatorProfileImageUrl: String
|
||||
}
|
||||
|
||||
struct GetAudioContentMainItem: Decodable {
|
||||
let contentId: Int
|
||||
let coverImageUrl: String
|
||||
@@ -55,13 +38,6 @@ struct GetAudioContentMainItem: Decodable {
|
||||
let isPointAvailable: Bool
|
||||
}
|
||||
|
||||
struct GetAudioContentCurationResponse: Decodable {
|
||||
let curationId: Int
|
||||
let title: String
|
||||
let description: String
|
||||
let contents: [GetAudioContentMainItem]
|
||||
}
|
||||
|
||||
struct GetAudioContentBannerResponse: Decodable {
|
||||
let type: AudioContentBannerType
|
||||
let thumbnailImageUrl: String
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
//
|
||||
// ContentMainNewContentThemeView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/11.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainNewContentThemeView: View {
|
||||
let themes: [String]
|
||||
let selectTheme: (String) -> Void
|
||||
|
||||
@Binding var selectedTheme: String
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(0..<themes.count, id: \.self) { index in
|
||||
let theme = themes[index]
|
||||
Text(theme)
|
||||
.appFont(size: 14.7, weight: .medium)
|
||||
.foregroundColor(Color(hex: selectedTheme == theme ? "3bb9f1" : "777777"))
|
||||
.padding(.horizontal, 13.3)
|
||||
.padding(.vertical, 9.3)
|
||||
.border(
|
||||
Color(hex: selectedTheme == theme ? "3bb9f1" : "eeeeee"),
|
||||
width: 0.5
|
||||
)
|
||||
.cornerRadius(16.7)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: CGFloat(16.7))
|
||||
.stroke(lineWidth: 0.5)
|
||||
.foregroundColor(Color(hex: selectedTheme == theme ? "3bb9f1" : "eeeeee"))
|
||||
)
|
||||
.onTapGesture {
|
||||
if selectedTheme != theme {
|
||||
selectTheme(theme)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentMainNewContentThemeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentMainNewContentThemeView(
|
||||
themes: ["전체", "테마1", "테마2"],
|
||||
selectTheme: { _ in },
|
||||
selectedTheme: .constant("전체")
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
//
|
||||
// ContentMainNewContentView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/11.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainNewContentView: View {
|
||||
|
||||
@StateObject private var viewModel = ContentMainNewContentViewModel()
|
||||
|
||||
var body: some View {
|
||||
LazyVStack(spacing: 16.7) {
|
||||
HStack(spacing: 0) {
|
||||
Text("새로운 콘텐츠")
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("ic_forward")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(step: .newContentAll(isFree: false))
|
||||
}
|
||||
}
|
||||
|
||||
if !viewModel.themeList.isEmpty {
|
||||
ContentMainNewContentThemeView(
|
||||
themes: viewModel.themeList,
|
||||
selectTheme: { theme in
|
||||
viewModel.selectedTheme = theme
|
||||
},
|
||||
selectedTheme: $viewModel.selectedTheme
|
||||
)
|
||||
}
|
||||
|
||||
if !viewModel.newContentList.isEmpty {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack(alignment: .top, spacing: 13.3) {
|
||||
ForEach(0..<viewModel.newContentList.count, id: \.self) {
|
||||
ContentMainItemView(item: viewModel.newContentList[$0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if viewModel.isLoading {
|
||||
ActivityIndicatorView()
|
||||
.frame(width: 100, height: 100)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.onAppear {
|
||||
viewModel.getThemeList()
|
||||
viewModel.getNewContentOfTheme()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentMainNewContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentMainNewContentView()
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
//
|
||||
// ContentMainNewContentViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/12/11.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class ContentMainNewContentViewModel: 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 {
|
||||
newContentList.removeAll()
|
||||
getNewContentOfTheme()
|
||||
}
|
||||
}
|
||||
|
||||
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.removeAll()
|
||||
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)
|
||||
}
|
||||
|
||||
func getNewContentOfTheme() {
|
||||
isLoading = true
|
||||
|
||||
repository.getNewContentOfTheme(theme: selectedTheme == "전체" ? "" : selectedTheme)
|
||||
.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<[GetAudioContentMainItem]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.newContentList.removeAll()
|
||||
self.newContentList.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)
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
//
|
||||
// ContentMainMyStashView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/11.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainMyStashView: View {
|
||||
|
||||
@StateObject private var viewModel = ContentMainMyStashViewModel()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if !viewModel.orderList.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
HStack(spacing: 0) {
|
||||
Text("내 보관함")
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("전체보기")
|
||||
.appFont(size: 11.3, weight: .light)
|
||||
.foregroundColor(Color(hex: "bbbbbb"))
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(step: .orderListAll)
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack(alignment: .top, spacing: 13.3) {
|
||||
ForEach(0..<viewModel.orderList.count, id: \.self) { index in
|
||||
let item = viewModel.orderList[index]
|
||||
ContentMainItemView(item: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 40)
|
||||
}
|
||||
|
||||
if viewModel.isLoading {
|
||||
ActivityIndicatorView()
|
||||
.frame(width: 100, height: 100)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.onAppear {
|
||||
viewModel.getOrderList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentMainMyStashView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentMainMyStashView()
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
//
|
||||
// ContentMainMyStashViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/12/11.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class ContentMainMyStashViewModel: ObservableObject {
|
||||
|
||||
private let repository = ContentRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var isLoading = false
|
||||
|
||||
@Published var orderList = [GetAudioContentMainItem]()
|
||||
|
||||
func getOrderList() {
|
||||
isLoading = true
|
||||
|
||||
repository.getMainOrderList()
|
||||
.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<[GetAudioContentMainItem]>.self, from: responseData)
|
||||
self.isLoading = false
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.orderList.removeAll()
|
||||
self.orderList.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
|
||||
self.isLoading = false
|
||||
}
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
//
|
||||
// ContentMainCreatorRankingView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 1/5/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct ContentMainCreatorRankingView: View {
|
||||
|
||||
@StateObject var viewModel = ContentMainCreatorRankingViewModel()
|
||||
|
||||
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 {
|
||||
ZStack {
|
||||
if let response = viewModel.creatorRankingResponse {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
if let coloredTitle = response.coloredTitle, let color = response.color {
|
||||
let titleArray = response.title.components(separatedBy: coloredTitle)
|
||||
HStack(spacing: 0) {
|
||||
Text(titleArray[0])
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
Text(coloredTitle)
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(Color(hex: color))
|
||||
|
||||
if titleArray.count > 1 {
|
||||
Text(titleArray[1])
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(Color.grayee)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(response.title)
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(Color.grayee)
|
||||
}
|
||||
|
||||
if let desc = response.desc {
|
||||
VStack(spacing: 8) {
|
||||
Text("\(desc)")
|
||||
.appFont(size: 14.7, weight: .bold)
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
Text("※ 인기 크리에이터의 순위는 매주 업데이트됩니다.")
|
||||
.appFont(size: 13.3, weight: .light)
|
||||
.foregroundColor(Color.graybb)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.gray22)
|
||||
.padding(.top, 13.3)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(alignment: .leading)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 13.3) {
|
||||
ForEach(0..<response.creators.count, id: \.self) { index in
|
||||
let creator = response.creators[index]
|
||||
VStack(spacing: 0) {
|
||||
if let _ = response.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)
|
||||
.appFont(size: 11.3, weight: .medium)
|
||||
.foregroundColor(Color.grayee)
|
||||
.lineLimit(1)
|
||||
.frame(width: 93.3)
|
||||
.padding(.top, 13.3)
|
||||
|
||||
Text(creator.tags)
|
||||
.appFont(size: 10, weight: .medium)
|
||||
.foregroundColor(Color.button)
|
||||
.lineLimit(1)
|
||||
.frame(width: 93.3)
|
||||
.padding(.top, 3.3)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
AppState.shared
|
||||
.setAppStep(step: .creatorDetail(userId: creator.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(alignment: .leading)
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
.frame(width: 0, height: 0)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.getCreatorRanking()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainCreatorRankingView()
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
//
|
||||
// ContentMainCreatorRankingViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 1/5/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class ContentMainCreatorRankingViewModel: ObservableObject {
|
||||
|
||||
private let repository = ContentRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var creatorRankingResponse: GetExplorerSectionResponse? = nil
|
||||
|
||||
func getCreatorRanking() {
|
||||
repository.getCreatorRanking()
|
||||
.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<GetExplorerSectionResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.creatorRankingResponse = 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)
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
//
|
||||
// ContentMainRankingView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/10/15.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct ContentMainRankingView: View {
|
||||
|
||||
@StateObject private var viewModel = ContentMainRankingViewModel()
|
||||
|
||||
let rows = [
|
||||
GridItem(.fixed(60), alignment: .leading),
|
||||
GridItem(.fixed(60), alignment: .leading),
|
||||
GridItem(.fixed(60), alignment: .leading)
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
LazyVStack(spacing: 16.7) {
|
||||
HStack(spacing: 0) {
|
||||
Text("인기 콘텐츠")
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("ic_forward")
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(step: .contentRankingAll)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text("\(viewModel.dateString)")
|
||||
.appFont(size: 14.7, weight: .bold)
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
|
||||
Text("※ 인기 콘텐츠의 순위는 매주 업데이트됩니다.")
|
||||
.appFont(size: 13.3, weight: .light)
|
||||
.foregroundColor(Color(hex: "bbbbbb"))
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
.frame(width: screenSize().width - 26.7)
|
||||
.background(Color(hex: "222222"))
|
||||
|
||||
if !viewModel.contentRankingSortList.isEmpty {
|
||||
ContentMainRankingSortView(
|
||||
sorts: viewModel.contentRankingSortList,
|
||||
selectSort: { viewModel.selectedContentRankingSort = $0 },
|
||||
selectedSort: $viewModel.selectedContentRankingSort
|
||||
)
|
||||
}
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHGrid(rows: rows, spacing: 13.3) {
|
||||
ForEach(0..<viewModel.contentRankingItemList.count, id: \.self) { index in
|
||||
let content = viewModel.contentRankingItemList[index]
|
||||
HStack(spacing: 0) {
|
||||
KFImage(URL(string: content.coverImageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(
|
||||
size: CGSize(
|
||||
width: 60,
|
||||
height: 60
|
||||
)
|
||||
)
|
||||
.resizable()
|
||||
.frame(width: 60, height: 60)
|
||||
.cornerRadius(2.7)
|
||||
|
||||
Text("\(index + 1)")
|
||||
.appFont(size: 16.7, weight: .bold)
|
||||
.foregroundColor(Color(hex: "3bb9f1"))
|
||||
.padding(.horizontal, 12)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(content.title)
|
||||
.lineLimit(2)
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(Color(hex: "d2d2d2"))
|
||||
|
||||
Text(content.creatorNickname)
|
||||
.appFont(size: 11, weight: .medium)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.onAppear {
|
||||
viewModel.getContentRankingSortType()
|
||||
viewModel.getContentRanking()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentMainRankingView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentMainRankingView()
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
//
|
||||
// ContentMainRankingViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/12/11.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class ContentMainRankingViewModel: 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 contentRankingSortList = [String]()
|
||||
@Published var contentRankingItemList = [GetAudioContentRankingItem]()
|
||||
|
||||
@Published var selectedContentRankingSort = "매출" {
|
||||
didSet {
|
||||
getContentRanking()
|
||||
}
|
||||
}
|
||||
|
||||
func getContentRankingSortType() {
|
||||
repository.getContentRankingSortType()
|
||||
.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.contentRankingSortList.removeAll()
|
||||
self.contentRankingSortList.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)
|
||||
}
|
||||
|
||||
func getContentRanking() {
|
||||
isLoading = true
|
||||
|
||||
repository.getContentRanking(page: 1, size: 12, sortType: selectedContentRankingSort)
|
||||
.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)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
dateString = "\(data.startDate)~\(data.endDate)"
|
||||
self.contentRankingItemList.removeAll()
|
||||
self.contentRankingItemList.append(contentsOf: data.items)
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
//
|
||||
// ContentMainRecommendSeriesView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 5/7/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainRecommendSeriesView: View {
|
||||
|
||||
@StateObject private var viewModel = ContentMainRecommendSeriesViewModel()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if !viewModel.seriesList.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text("추천 시리즈")
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(alignment: .top, spacing: 13.3) {
|
||||
ForEach(0..<viewModel.seriesList.count, id: \.self) {
|
||||
let item = viewModel.seriesList[$0]
|
||||
SeriesListBigItemView(item: item, isVisibleCreator: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Image("ic_refresh")
|
||||
|
||||
Text("새로고침")
|
||||
.appFont(size: 14.7, weight: .medium)
|
||||
.foregroundColor(Color.grayd2)
|
||||
}
|
||||
.padding(.vertical, 11)
|
||||
.frame(maxWidth: .infinity)
|
||||
.contentShape(Rectangle())
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 26.7)
|
||||
.stroke(Color.gray90, lineWidth: 1)
|
||||
)
|
||||
.onTapGesture {
|
||||
viewModel.getRecommendSeriesList()
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 26.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.getRecommendSeriesList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainRecommendSeriesView()
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
//
|
||||
// ContentMainRecommendSeriesViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 5/7/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class ContentMainRecommendSeriesViewModel: ObservableObject {
|
||||
|
||||
private let repository = SeriesRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var isLoading = false
|
||||
|
||||
@Published var seriesList = [SeriesListItem]()
|
||||
|
||||
func getRecommendSeriesList() {
|
||||
repository.getRecommendSeriesList()
|
||||
.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<[SeriesListItem]>.self, from: responseData)
|
||||
self.isLoading = false
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.seriesList.removeAll()
|
||||
self.seriesList.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
|
||||
self.isLoading = false
|
||||
}
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
//
|
||||
// ContentMainAlarmAllView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainAlarmAllView: View {
|
||||
|
||||
@StateObject var viewModel = ContentMainAlarmAllViewModel()
|
||||
@State private var isInitialized = false
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
DetailNavigationBar(title: "새로운 알람")
|
||||
|
||||
Text("※ 최근 2주간 등록된 새로운 알람 입니다.")
|
||||
.appFont(size: 14.7, weight: .medium)
|
||||
.foregroundColor(.graybb)
|
||||
.padding(.horizontal, 13.3)
|
||||
.padding(.vertical, 8)
|
||||
.frame(width: screenSize().width, alignment: .leading)
|
||||
.background(Color.gray22)
|
||||
|
||||
ContentMainNewContentThemeView(
|
||||
themes: viewModel.themeList,
|
||||
selectTheme: {
|
||||
viewModel.selectedTheme = $0
|
||||
},
|
||||
selectedTheme: $viewModel.selectedTheme
|
||||
)
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Text("전체")
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(Color(hex: "e2e2e2"))
|
||||
|
||||
Text("\(viewModel.totalCount)")
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(Color(hex: "ff5c49"))
|
||||
.padding(.leading, 8)
|
||||
|
||||
Text("개")
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(Color(hex: "e2e2e2"))
|
||||
.padding(.leading, 2)
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
let horizontalPadding: CGFloat = 16
|
||||
let gridSpacing: CGFloat = 16
|
||||
let itemSize = (screenSize().width - (horizontalPadding * 2) - gridSpacing) / 2
|
||||
|
||||
LazyVGrid(
|
||||
columns: Array(
|
||||
repeating: GridItem(
|
||||
.flexible(),
|
||||
spacing: gridSpacing,
|
||||
alignment: .topLeading
|
||||
),
|
||||
count: 2
|
||||
),
|
||||
alignment: .leading,
|
||||
spacing: gridSpacing
|
||||
) {
|
||||
ForEach(0..<viewModel.newContentList.count, id: \.self) { index in
|
||||
ContentNewAllItemView(width: itemSize, item: viewModel.newContentList[index])
|
||||
.onAppear {
|
||||
if index == viewModel.newContentList.count - 1 {
|
||||
viewModel.getContentMainAlarmAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if !isInitialized {
|
||||
viewModel.getContentMainAlarmAll()
|
||||
isInitialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainAlarmAllView()
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
//
|
||||
// ContentMainAlarmAllViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class ContentMainAlarmAllViewModel: ObservableObject {
|
||||
|
||||
private let repository = ContentMainTabAlarmRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var isLoading = false
|
||||
|
||||
@Published var themeList = ["전체", "모닝콜", "슬립콜", "알람"]
|
||||
@Published var newContentList = [GetAudioContentMainItem]()
|
||||
|
||||
@Published var selectedTheme = "전체" {
|
||||
didSet {
|
||||
page = 1
|
||||
isLast = false
|
||||
getContentMainAlarmAll()
|
||||
}
|
||||
}
|
||||
@Published var totalCount = 0
|
||||
|
||||
var page = 1
|
||||
var isLast = false
|
||||
private let pageSize = 20
|
||||
|
||||
func getContentMainAlarmAll() {
|
||||
if (!isLast && !isLoading) {
|
||||
isLoading = true
|
||||
|
||||
repository.getContentMainAlarmAll(
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
//
|
||||
// ContentMainTabAlarmRepository.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CombineMoya
|
||||
import Combine
|
||||
import Moya
|
||||
|
||||
final class ContentMainTabAlarmRepository {
|
||||
|
||||
private let api = MoyaProvider<ContentApi>()
|
||||
|
||||
func getContentMainAlarm() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getContentMainAlarm(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getContentMainAlarmAll(theme: String, page: Int = 1, size: Int = 10) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getContentMainAlarmAll(
|
||||
theme: theme,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
|
||||
page: page,
|
||||
size: size
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
//
|
||||
// ContentMainTabAlarmView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainTabAlarmView: View {
|
||||
|
||||
@StateObject var viewModel = ContentMainTabAlarmViewModel()
|
||||
|
||||
var body: some View {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(spacing: 0) {
|
||||
if !viewModel.bannerList.isEmpty {
|
||||
ContentMainBannerViewV2(bannerList: viewModel.bannerList)
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
|
||||
if !viewModel.alarmThemeList.isEmpty {
|
||||
ContentMainNewContentViewV2(
|
||||
title: "새로운 알람",
|
||||
onClickMore: {
|
||||
AppState.shared
|
||||
.setAppStep(step: .newAlarmContentAll)
|
||||
},
|
||||
themeList: viewModel.alarmThemeList,
|
||||
contentList: viewModel.newAlarmContentList
|
||||
) {
|
||||
viewModel.getContentMainAlarm(theme: $0)
|
||||
}
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.eventBannerList.isEmpty {
|
||||
SectionEventBannerView(items: viewModel.eventBannerList)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.curationList.isEmpty {
|
||||
ContentMainCurationViewV2(curationList: viewModel.curationList)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.fetchData()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainTabAlarmView()
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
//
|
||||
// ContentMainTabAlarmViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class ContentMainTabAlarmViewModel: ObservableObject {
|
||||
private let repository = ContentMainTabAlarmRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var isLoading = false
|
||||
|
||||
@Published var bannerList: [GetAudioContentBannerResponse] = []
|
||||
@Published var alarmThemeList: [String] = []
|
||||
@Published var newAlarmContentList: [GetAudioContentMainItem] = []
|
||||
@Published var rankAlarmContentList: [GetAudioContentRankingItem] = []
|
||||
@Published var eventBannerList: [EventItem] = []
|
||||
@Published var curationList: [GetContentCurationResponse] = []
|
||||
|
||||
func fetchData() {
|
||||
isLoading = true
|
||||
repository.getContentMainAlarm()
|
||||
.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<GetContentMainTabAlarmResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.bannerList = data.contentBannerList
|
||||
self.alarmThemeList = ["전체"] + data.alarmThemeList
|
||||
self.newAlarmContentList = data.newAlarmContentList
|
||||
self.rankAlarmContentList = data.rankAlarmContentList
|
||||
self.eventBannerList = data.eventBannerList.eventList
|
||||
self.curationList = data.curationList
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func getContentMainAlarm(theme: String) {
|
||||
isLoading = true
|
||||
repository.getContentMainAlarmAll(theme: theme)
|
||||
.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<GetNewContentAllResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.newAlarmContentList = data.items
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
//
|
||||
// GetContentMainTabAlarmResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GetContentMainTabAlarmResponse: Decodable {
|
||||
let contentBannerList: [GetAudioContentBannerResponse]
|
||||
let alarmThemeList: [String]
|
||||
let newAlarmContentList: [GetAudioContentMainItem]
|
||||
let rankAlarmContentList: [GetAudioContentRankingItem]
|
||||
let eventBannerList: GetEventResponse
|
||||
let curationList: [GetContentCurationResponse]
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
//
|
||||
// ContentMainAsmrAllView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainAsmrAllView: View {
|
||||
|
||||
@StateObject var viewModel = ContentNewAllViewModel()
|
||||
@State private var isInitialized = false
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
DetailNavigationBar(title: "새로운 ASMR")
|
||||
|
||||
Text("※ 최근 2주간 등록된 새로운 ASMR 입니다.")
|
||||
.appFont(size: 14.7, weight: .medium)
|
||||
.foregroundColor(.graybb)
|
||||
.padding(.horizontal, 13.3)
|
||||
.padding(.vertical, 8)
|
||||
.frame(width: screenSize().width, alignment: .leading)
|
||||
.background(Color.gray22)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Text("전체")
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(Color(hex: "e2e2e2"))
|
||||
|
||||
Text("\(viewModel.totalCount)")
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(Color(hex: "ff5c49"))
|
||||
.padding(.leading, 8)
|
||||
|
||||
Text("개")
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(Color(hex: "e2e2e2"))
|
||||
.padding(.leading, 2)
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
let horizontalPadding: CGFloat = 16
|
||||
let gridSpacing: CGFloat = 16
|
||||
let itemSize = (screenSize().width - (horizontalPadding * 2) - gridSpacing) / 2
|
||||
|
||||
LazyVGrid(
|
||||
columns: Array(
|
||||
repeating: GridItem(
|
||||
.flexible(),
|
||||
spacing: gridSpacing,
|
||||
alignment: .topLeading
|
||||
),
|
||||
count: 2
|
||||
),
|
||||
alignment: .leading,
|
||||
spacing: gridSpacing
|
||||
) {
|
||||
ForEach(0..<viewModel.newContentList.count, id: \.self) { index in
|
||||
ContentNewAllItemView(width: itemSize, item: viewModel.newContentList[index])
|
||||
.onAppear {
|
||||
if index == viewModel.newContentList.count - 1 {
|
||||
viewModel.getNewContentList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if !isInitialized {
|
||||
if viewModel.selectedTheme != "ASMR" {
|
||||
viewModel.selectedTheme = "ASMR"
|
||||
} else if viewModel.newContentList.isEmpty {
|
||||
viewModel.getNewContentList()
|
||||
}
|
||||
isInitialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainAsmrAllView()
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
//
|
||||
// ContentMainTabAsmrRepository.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CombineMoya
|
||||
import Combine
|
||||
import Moya
|
||||
|
||||
final class ContentMainTabAsmrRepository {
|
||||
|
||||
private let api = MoyaProvider<ContentApi>()
|
||||
|
||||
func getContentMainAsmr() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getContentMainAsmr(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getPopularContentByCreator(creatorId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getPopularAsmrContentByCreator(
|
||||
creatorId: creatorId,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
//
|
||||
// ContentMainTabAsmrView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainTabAsmrView: View {
|
||||
|
||||
@StateObject var viewModel = ContentMainTabAsmrViewModel()
|
||||
|
||||
var body: some View {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(spacing: 0) {
|
||||
if !viewModel.bannerList.isEmpty {
|
||||
ContentMainBannerViewV2(bannerList: viewModel.bannerList)
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
|
||||
if !viewModel.newAsmrContentList.isEmpty {
|
||||
ContentMainNewContentViewV2(
|
||||
title: "새로운 ASMR",
|
||||
onClickMore: {
|
||||
AppState.shared
|
||||
.setAppStep(step: .newAsmrContentAll)
|
||||
},
|
||||
themeList: [],
|
||||
contentList: viewModel.newAsmrContentList
|
||||
) { _ in }
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.creatorList.isEmpty {
|
||||
ContentByChannelView(
|
||||
title: "채널별 추천 ASMR",
|
||||
creatorList: viewModel.creatorList,
|
||||
contentList: viewModel.salesCountRankContentList,
|
||||
onClickCreator: {
|
||||
viewModel.getPopularContentByCreator(creatorId: $0)
|
||||
}
|
||||
)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.eventBannerList.isEmpty {
|
||||
SectionEventBannerView(items: viewModel.eventBannerList)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.curationList.isEmpty {
|
||||
ContentMainCurationViewV2(curationList: viewModel.curationList)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.fetchData()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainTabAsmrView()
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
//
|
||||
// ContentMainTabAsmrViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class ContentMainTabAsmrViewModel: ObservableObject {
|
||||
private let repository = ContentMainTabAsmrRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var isLoading = false
|
||||
|
||||
@Published var bannerList: [GetAudioContentBannerResponse] = []
|
||||
@Published var newAsmrContentList: [GetAudioContentMainItem] = []
|
||||
@Published var creatorList: [ContentCreatorResponse] = []
|
||||
@Published var salesCountRankContentList: [GetAudioContentRankingItem] = []
|
||||
@Published var eventBannerList: [EventItem] = []
|
||||
@Published var curationList: [GetContentCurationResponse] = []
|
||||
|
||||
func fetchData() {
|
||||
isLoading = true
|
||||
repository.getContentMainAsmr()
|
||||
.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<GetContentMainTabAsmrResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.bannerList = data.contentBannerList
|
||||
self.newAsmrContentList = data.newAsmrContentList
|
||||
self.creatorList = data.creatorList
|
||||
self.salesCountRankContentList = data.salesCountRankContentList
|
||||
self.eventBannerList = data.eventBannerList.eventList
|
||||
self.curationList = data.curationList
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func getPopularContentByCreator(creatorId: Int) {
|
||||
isLoading = true
|
||||
repository.getPopularContentByCreator(creatorId: creatorId)
|
||||
.sink { result in
|
||||
switch result {
|
||||
case .finished:
|
||||
DEBUG_LOG("finish")
|
||||
case .failure(let error):
|
||||
ERROR_LOG(error.localizedDescription)
|
||||
}
|
||||
} receiveValue: { [unowned self] response in
|
||||
let responseData = response.data
|
||||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<[GetAudioContentRankingItem]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.salesCountRankContentList = data
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
//
|
||||
// GetContentMainTabAsmrResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
struct GetContentMainTabAsmrResponse: Decodable {
|
||||
let contentBannerList: [GetAudioContentBannerResponse]
|
||||
let newAsmrContentList: [GetAudioContentMainItem]
|
||||
let creatorList: [ContentCreatorResponse]
|
||||
let salesCountRankContentList: [GetAudioContentRankingItem]
|
||||
let eventBannerList: GetEventResponse
|
||||
let curationList: [GetContentCurationResponse]
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
//
|
||||
// ContentMainTabContentRepository.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CombineMoya
|
||||
import Combine
|
||||
import Moya
|
||||
|
||||
final class ContentMainTabContentRepository {
|
||||
|
||||
private let api = MoyaProvider<ContentApi>()
|
||||
|
||||
func getContentMainContent() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getContentMainContent(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getNewContentOfTheme(theme: String) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getContentMainNewContentOfTheme(
|
||||
theme: theme,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getContentRanking(sortType: String) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getDailyContentRanking(
|
||||
sortType: sortType,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getRecommendContentByTag(tag: String) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getRecommendContentByTag(
|
||||
tag: tag,
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getPopularContentByCreator(creatorId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getContentMainContentPopularContentByCreator(
|
||||
creatorId: creatorId,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
//
|
||||
// ContentMainTabContentView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainTabContentView: View {
|
||||
|
||||
@StateObject var viewModel = ContentMainTabContentViewModel()
|
||||
|
||||
var body: some View {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(spacing: 0) {
|
||||
if !viewModel.bannerList.isEmpty {
|
||||
ContentMainBannerViewV2(bannerList: viewModel.bannerList)
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
|
||||
if !viewModel.contentThemeList.isEmpty {
|
||||
ContentMainNewContentViewV2(
|
||||
title: "새로운 단편",
|
||||
onClickMore: {
|
||||
AppState.shared
|
||||
.setAppStep(step: .newContentAll(isFree: false))
|
||||
},
|
||||
themeList: viewModel.contentThemeList,
|
||||
contentList: viewModel.newContentList
|
||||
) {
|
||||
viewModel.getNewContentOfTheme(theme: $0)
|
||||
}
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.rankSortTypeList.isEmpty {
|
||||
ContentMainTabRankContentView(
|
||||
title: "일간 랭킹",
|
||||
isMore: false,
|
||||
onClickMore: {},
|
||||
sortList: viewModel.rankSortTypeList,
|
||||
onClickSort: { viewModel.getContentRanking(sort: $0) },
|
||||
contentList: viewModel.rankContentList
|
||||
)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.contentRankCreatorList.isEmpty {
|
||||
ContentByChannelView(
|
||||
title: "채널별 추천 단편",
|
||||
creatorList: viewModel.contentRankCreatorList,
|
||||
contentList: viewModel.likeCountRankContentList,
|
||||
onClickCreator: {
|
||||
viewModel.getPopularContentByCreator(creatorId: $0)
|
||||
}
|
||||
)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.eventBannerList.isEmpty {
|
||||
SectionEventBannerView(items: viewModel.eventBannerList)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.tagList.isEmpty {
|
||||
ContentMainTagCurationView(
|
||||
tagList: viewModel.tagList,
|
||||
contentList: viewModel.tagCurationContentList
|
||||
) {
|
||||
viewModel.getRecommendContentByTag(tag: $0)
|
||||
}
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.curationList.isEmpty {
|
||||
ContentMainCurationViewV2(curationList: viewModel.curationList)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.fetchData()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainTabContentView()
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
//
|
||||
// ContentMainTabContentViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class ContentMainTabContentViewModel: ObservableObject {
|
||||
private let repository = ContentMainTabContentRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var isLoading = false
|
||||
|
||||
@Published var bannerList: [GetAudioContentBannerResponse] = []
|
||||
@Published var contentThemeList: [String] = []
|
||||
@Published var newContentList: [GetAudioContentMainItem] = []
|
||||
@Published var rankSortTypeList: [String] = []
|
||||
@Published var rankContentList: [GetAudioContentRankingItem] = []
|
||||
@Published var contentRankCreatorList: [ContentCreatorResponse] = []
|
||||
@Published var likeCountRankContentList: [GetAudioContentRankingItem] = []
|
||||
@Published var eventBannerList: [EventItem] = []
|
||||
@Published var tagList: [String] = []
|
||||
@Published var tagCurationContentList: [GetAudioContentMainItem] = []
|
||||
@Published var curationList: [GetContentCurationResponse] = []
|
||||
|
||||
func fetchData() {
|
||||
isLoading = true
|
||||
repository.getContentMainContent()
|
||||
.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<GetContentMainTabContentResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.bannerList = data.bannerList
|
||||
self.contentThemeList = ["전체"] + data.contentThemeList
|
||||
self.newContentList = data.newContentList
|
||||
self.rankSortTypeList = data.rankSortTypeList
|
||||
self.rankContentList = data.rankContentList
|
||||
self.contentRankCreatorList = data.contentRankCreatorList
|
||||
self.likeCountRankContentList = data.likeCountRankContentList
|
||||
self.eventBannerList = data.eventBannerList.eventList
|
||||
self.tagList = data.tagList
|
||||
self.tagCurationContentList = data.tagCurationContentList
|
||||
self.curationList = data.curationList
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func getNewContentOfTheme(theme: String) {
|
||||
isLoading = true
|
||||
|
||||
repository.getNewContentOfTheme(theme: theme == "전체" ? "" : theme)
|
||||
.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<[GetAudioContentMainItem]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.newContentList = data
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func getContentRanking(sort: String = "매출") {
|
||||
isLoading = true
|
||||
repository.getContentRanking(sortType: 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<[GetAudioContentRankingItem]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.rankContentList = data
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func getRecommendContentByTag(tag: String) {
|
||||
isLoading = true
|
||||
repository.getRecommendContentByTag(tag: tag)
|
||||
.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<[GetAudioContentMainItem]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.tagCurationContentList = data
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func getPopularContentByCreator(creatorId: Int) {
|
||||
isLoading = true
|
||||
repository.getPopularContentByCreator(creatorId: creatorId)
|
||||
.sink { result in
|
||||
switch result {
|
||||
case .finished:
|
||||
DEBUG_LOG("finish")
|
||||
case .failure(let error):
|
||||
ERROR_LOG(error.localizedDescription)
|
||||
}
|
||||
} receiveValue: { [unowned self] response in
|
||||
let responseData = response.data
|
||||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<[GetAudioContentRankingItem]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.likeCountRankContentList = data
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
//
|
||||
// ContentMainTabRankContentView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/20/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
import Kingfisher
|
||||
|
||||
struct ContentMainTabRankContentView: View {
|
||||
|
||||
@AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
|
||||
|
||||
let rows = [
|
||||
GridItem(.fixed(60), alignment: .leading),
|
||||
GridItem(.fixed(60), alignment: .leading),
|
||||
GridItem(.fixed(60), alignment: .leading)
|
||||
]
|
||||
|
||||
let title: String
|
||||
|
||||
let isMore: Bool
|
||||
let onClickMore: () -> Void
|
||||
|
||||
let sortList: [String]
|
||||
let onClickSort: (String) -> Void
|
||||
|
||||
let contentList: [GetAudioContentRankingItem]
|
||||
|
||||
@State private var selectedSort = ""
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 13.3) {
|
||||
HStack(spacing: 0) {
|
||||
Text(title)
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
|
||||
Spacer()
|
||||
|
||||
if isMore {
|
||||
Image("ic_forward")
|
||||
.onTapGesture { onClickMore() }
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
if !sortList.isEmpty {
|
||||
ContentMainRankingSortView(
|
||||
sorts: sortList,
|
||||
selectSort: {
|
||||
selectedSort = $0
|
||||
onClickSort($0)
|
||||
},
|
||||
selectedSort: $selectedSort
|
||||
)
|
||||
}
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHGrid(rows: rows, spacing: 13.3) {
|
||||
ForEach(0..<contentList.count, id: \.self) { index in
|
||||
let content = contentList[index]
|
||||
HStack(spacing: 0) {
|
||||
KFImage(URL(string: content.coverImageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(
|
||||
size: CGSize(
|
||||
width: 60,
|
||||
height: 60
|
||||
)
|
||||
)
|
||||
.resizable()
|
||||
.frame(width: 60, height: 60)
|
||||
.cornerRadius(2.7)
|
||||
|
||||
Text("\(index + 1)")
|
||||
.appFont(size: 16.7, weight: .bold)
|
||||
.foregroundColor(.button)
|
||||
.padding(.horizontal, 12)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(content.title)
|
||||
.lineLimit(2)
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(.grayd2)
|
||||
|
||||
Text(content.creatorNickname)
|
||||
.appFont(size: 11, weight: .medium)
|
||||
.foregroundColor(.gray77)
|
||||
}
|
||||
}
|
||||
.frame(width: screenSize().width * 0.66, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
AppState.shared
|
||||
.setAppStep(step: .contentDetail(contentId: content.contentId))
|
||||
} else {
|
||||
AppState.shared
|
||||
.setAppStep(step: .login)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
.frame(height: 207)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if !sortList.isEmpty {
|
||||
selectedSort = sortList[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainTabRankContentView(
|
||||
title: "인기 단편",
|
||||
isMore: true,
|
||||
onClickMore: {},
|
||||
sortList: ["매출", "댓글", "좋아요"],
|
||||
onClickSort: { _ in },
|
||||
contentList: [
|
||||
GetAudioContentRankingItem(
|
||||
contentId: 1,
|
||||
title: "안녕하세요 오늘은 커버곡을 들려드릴께요....안녕하세요 오늘은 커버곡을 들려드릴께요....",
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
themeStr: "커버곡",
|
||||
price: 100,
|
||||
duration: "00:30:20",
|
||||
creatorId: 1,
|
||||
creatorNickname: "유저1",
|
||||
isPointAvailable: false,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
GetAudioContentRankingItem(
|
||||
contentId: 2,
|
||||
title: "안녕하세요 오늘은 커버곡을 들려드릴께요....",
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
themeStr: "커버곡",
|
||||
price: 0,
|
||||
duration: "00:30:20",
|
||||
creatorId: 2,
|
||||
creatorNickname: "유저2",
|
||||
isPointAvailable: false,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
GetAudioContentRankingItem(
|
||||
contentId: 3,
|
||||
title: "안녕하세요 오늘은 커버곡을 들려드릴께요....안녕하세요 오늘은 커버곡을 들려드릴께요....",
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
themeStr: "커버곡",
|
||||
price: 50,
|
||||
duration: "00:30:20",
|
||||
creatorId: 3,
|
||||
creatorNickname: "유저3",
|
||||
isPointAvailable: true,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
//
|
||||
// ContentMainTagCurationView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct ContentMainTagCurationView: View {
|
||||
|
||||
let tagList: [String]
|
||||
let contentList: [GetAudioContentMainItem]
|
||||
let selectTag: (String) -> Void
|
||||
|
||||
let tagColumns = [
|
||||
GridItem(.flexible()),
|
||||
GridItem(.flexible()),
|
||||
GridItem(.flexible()),
|
||||
GridItem(.flexible())
|
||||
]
|
||||
|
||||
let contentColumns = [
|
||||
GridItem(.flexible()),
|
||||
GridItem(.flexible()),
|
||||
GridItem(.flexible())
|
||||
]
|
||||
|
||||
@State private var selectedTag = ""
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text("태그별 추천 콘텐츠")
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(.grayee)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
LazyVGrid(columns: tagColumns, spacing: 6) {
|
||||
ForEach(0..<tagList.count, id: \.self) { index in
|
||||
let tag = tagList[index]
|
||||
Text(tagList[index])
|
||||
.appFont(size: 10, weight: .medium)
|
||||
.foregroundColor(
|
||||
selectedTag == tag ?
|
||||
.button:
|
||||
.gray77
|
||||
)
|
||||
.padding(.vertical, 10)
|
||||
.frame(width: (screenSize().width - 18 - 26.7) / 4)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 2.6)
|
||||
.strokeBorder(lineWidth: 1)
|
||||
.foregroundColor(
|
||||
selectedTag == tag ?
|
||||
.button:
|
||||
.gray77
|
||||
)
|
||||
)
|
||||
.onTapGesture {
|
||||
if selectedTag != tag {
|
||||
selectedTag = tag
|
||||
selectTag(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
LazyVGrid(columns: contentColumns, spacing: 13.3) {
|
||||
ForEach(0..<contentList.count, id: \.self) { index in
|
||||
ContentMainTagCurationContentView(
|
||||
item: contentList[index],
|
||||
itemWidth: (screenSize().width - 40) / 3
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
.onAppear {
|
||||
selectedTag = tagList[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentMainTagCurationContentView: View {
|
||||
|
||||
let item: GetAudioContentMainItem
|
||||
let itemWidth: CGFloat
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ZStack(alignment: .bottom) {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
KFImage(URL(string: item.coverImageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(
|
||||
size: CGSize(
|
||||
width: itemWidth,
|
||||
height: itemWidth
|
||||
)
|
||||
)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: itemWidth, height: itemWidth, alignment: .top)
|
||||
.cornerRadius(2.7)
|
||||
|
||||
if item.isPointAvailable {
|
||||
Image("ic_point")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
.padding(.top, 2.7)
|
||||
.padding(.trailing, 2.7)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 0) {
|
||||
HStack(spacing: 2) {
|
||||
if item.price > 0 {
|
||||
Image("ic_card_can_gray")
|
||||
|
||||
Text("\(item.price)")
|
||||
.appFont(size: 8.5, weight: .medium)
|
||||
.foregroundColor(Color.white)
|
||||
} else {
|
||||
Text("무료")
|
||||
.appFont(size: 8.5, weight: .medium)
|
||||
.foregroundColor(Color.white)
|
||||
}
|
||||
}
|
||||
.padding(3)
|
||||
.background(Color.gray33.opacity(0.7))
|
||||
.cornerRadius(10)
|
||||
.padding(.leading, 2.7)
|
||||
.padding(.bottom, 2.7)
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 2) {
|
||||
Text(item.duration)
|
||||
.appFont(size: 8.5, weight: .medium)
|
||||
.foregroundColor(Color.white)
|
||||
}
|
||||
.padding(3)
|
||||
.background(Color.gray33.opacity(0.7))
|
||||
.cornerRadius(10)
|
||||
.padding(.trailing, 2.7)
|
||||
.padding(.bottom, 2.7)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: itemWidth, height: itemWidth)
|
||||
|
||||
Text(item.title)
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(Color.grayd2)
|
||||
.frame(width: itemWidth, alignment: .leading)
|
||||
.multilineTextAlignment(.leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.lineLimit(2)
|
||||
|
||||
HStack(spacing: 5.3) {
|
||||
KFImage(URL(string: item.creatorProfileImageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(
|
||||
size: CGSize(
|
||||
width: 21.3,
|
||||
height: 21.3
|
||||
)
|
||||
)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 21.3, height: 21.3)
|
||||
.clipShape(Circle())
|
||||
.onTapGesture { AppState.shared.setAppStep(step: .creatorDetail(userId: item.creatorId)) }
|
||||
|
||||
Text(item.creatorNickname)
|
||||
.appFont(size: 12, weight: .medium)
|
||||
.foregroundColor(.gray77)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.padding(.bottom, 10)
|
||||
}
|
||||
.onTapGesture {
|
||||
AppState.shared
|
||||
.setAppStep(step: .contentDetail(contentId: item.contentId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainTagCurationView(
|
||||
tagList: ["test", "test2", "test3", "test4", "test5", "test6", "test7"],
|
||||
contentList: [
|
||||
GetAudioContentMainItem(
|
||||
contentId: 1,
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
title: "ㅓ처랴햐햫햐햐",
|
||||
creatorId: 8,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorNickname: "유저1",
|
||||
price: 100,
|
||||
duration: "00:00:30",
|
||||
isPointAvailable: true
|
||||
),
|
||||
GetAudioContentMainItem(
|
||||
contentId: 2,
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
title: "ㅓ처랴햐햫햐햐",
|
||||
creatorId: 8,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorNickname: "유저2",
|
||||
price: 0,
|
||||
duration: "00:00:30",
|
||||
isPointAvailable: false
|
||||
),
|
||||
GetAudioContentMainItem(
|
||||
contentId: 3,
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
title: "ㅓ처랴햐햫햐햐",
|
||||
creatorId: 8,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorNickname: "유저3",
|
||||
price: 1000,
|
||||
duration: "00:00:30",
|
||||
isPointAvailable: false
|
||||
),
|
||||
GetAudioContentMainItem(
|
||||
contentId: 4,
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
title: "ㅓ처랴햐햫햐햐",
|
||||
creatorId: 8,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorNickname: "유저3",
|
||||
price: 50000,
|
||||
duration: "00:00:30",
|
||||
isPointAvailable: false
|
||||
)
|
||||
],
|
||||
selectTag: { _ in }
|
||||
)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
//
|
||||
// GetContentMainTabContentResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
struct GetContentMainTabContentResponse: Decodable {
|
||||
let bannerList: [GetAudioContentBannerResponse]
|
||||
let contentThemeList: [String]
|
||||
let newContentList: [GetAudioContentMainItem]
|
||||
let rankSortTypeList: [String]
|
||||
let rankContentList: [GetAudioContentRankingItem]
|
||||
let contentRankCreatorList: [ContentCreatorResponse]
|
||||
let likeCountRankContentList: [GetAudioContentRankingItem]
|
||||
let eventBannerList: GetEventResponse
|
||||
let tagList: [String]
|
||||
let tagCurationContentList: [GetAudioContentMainItem]
|
||||
let curationList: [GetContentCurationResponse]
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
//
|
||||
// ContentByChannelView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/20/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct ContentByChannelView: View {
|
||||
|
||||
@AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
|
||||
|
||||
let title: String
|
||||
let creatorList: [ContentCreatorResponse]
|
||||
let contentList: [GetAudioContentRankingItem]
|
||||
let onClickCreator: (Int) -> Void
|
||||
|
||||
@State private var selectedCreatorId = 0
|
||||
|
||||
let columns = [
|
||||
GridItem(.flexible(), spacing: 13.3),
|
||||
GridItem(.flexible(), spacing: 13.3)
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Text(title)
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(.grayee)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 22) {
|
||||
ForEach(0..<creatorList.count, id: \.self) { index in
|
||||
let item = creatorList[index]
|
||||
|
||||
ContentCreatorView(
|
||||
isSelected: item.creatorId == selectedCreatorId,
|
||||
item: item
|
||||
)
|
||||
.onTapGesture {
|
||||
let creatorId = item.creatorId
|
||||
selectedCreatorId = creatorId
|
||||
onClickCreator(creatorId)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
|
||||
LazyVGrid(columns: columns, spacing: 13.3) {
|
||||
ForEach(0..<contentList.count, id: \.self) { index in
|
||||
let content = contentList[index]
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ZStack(alignment:.bottom) {
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .topTrailing) {
|
||||
KFImage(URL(string: content.coverImageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: geometry.size.width, height: geometry.size.width)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 5.3))
|
||||
.clipped()
|
||||
|
||||
if content.isPointAvailable {
|
||||
Image("ic_point")
|
||||
.padding(.top, 2.7)
|
||||
.padding(.trailing, 2.7)
|
||||
}
|
||||
}
|
||||
}
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
HStack(spacing: 2) {
|
||||
if content.price > 0 {
|
||||
Image("ic_card_can_gray_32")
|
||||
}
|
||||
|
||||
Text(content.price > 0 ? "\(content.price)" : "무료")
|
||||
.appFont(size: 12, weight: .medium)
|
||||
.foregroundColor(Color.white)
|
||||
}
|
||||
.padding(4)
|
||||
.background(Color.gray33.opacity(0.7))
|
||||
.cornerRadius(10)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(content.duration)
|
||||
.appFont(size: 12, weight: .medium)
|
||||
.foregroundColor(Color.white)
|
||||
.padding(4)
|
||||
.background(Color.gray33.opacity(0.7))
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.padding(.horizontal, 2.7)
|
||||
.padding(.bottom, 2.7)
|
||||
}
|
||||
|
||||
Text(content.title)
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(.grayd2)
|
||||
.lineLimit(1)
|
||||
|
||||
HStack(spacing: 5.3) {
|
||||
KFImage(URL(string: content.creatorProfileImageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(size: CGSize(width: 21, height: 21))
|
||||
.resizable()
|
||||
.frame(width: 21, height: 21)
|
||||
.clipShape(Circle())
|
||||
.clipped()
|
||||
|
||||
Text(content.creatorNickname)
|
||||
.appFont(size: 10, weight: .medium)
|
||||
.foregroundColor(.gray77)
|
||||
}
|
||||
.onTapGesture {
|
||||
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
AppState.shared
|
||||
.setAppStep(step: .creatorDetail(userId: content.creatorId))
|
||||
} else {
|
||||
AppState.shared
|
||||
.setAppStep(step: .login)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
AppState.shared
|
||||
.setAppStep(step: .contentDetail(contentId: content.contentId))
|
||||
} else {
|
||||
AppState.shared
|
||||
.setAppStep(step: .login)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
.onAppear {
|
||||
if !self.creatorList.isEmpty {
|
||||
selectedCreatorId = creatorList[0].creatorId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentByChannelView(
|
||||
title: "채널별 인기 콘텐츠",
|
||||
creatorList: [
|
||||
ContentCreatorResponse(
|
||||
creatorId: 1,
|
||||
creatorNickname: "유저1",
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
ContentCreatorResponse(
|
||||
creatorId: 2,
|
||||
creatorNickname: "유저2",
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
)
|
||||
],
|
||||
contentList: [
|
||||
GetAudioContentRankingItem(
|
||||
contentId: 1,
|
||||
title: "안녕하세요 오늘은 커버곡을 들려드릴께요....안녕하세요 오늘은 커버곡을 들려드릴께요....",
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
themeStr: "커버곡",
|
||||
price: 100,
|
||||
duration: "00:30:20",
|
||||
creatorId: 1,
|
||||
creatorNickname: "유저1",
|
||||
isPointAvailable: true,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
GetAudioContentRankingItem(
|
||||
contentId: 2,
|
||||
title: "안녕하세요 오늘은 커버곡을 들려드릴께요....",
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
themeStr: "커버곡",
|
||||
price: 0,
|
||||
duration: "00:30:20",
|
||||
creatorId: 1,
|
||||
creatorNickname: "유저1",
|
||||
isPointAvailable: false,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
GetAudioContentRankingItem(
|
||||
contentId: 3,
|
||||
title: "안녕하세요 오늘은 커버곡을 들려드릴께요....안녕하세요 오늘은 커버곡을 들려드릴께요....",
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
themeStr: "커버곡",
|
||||
price: 50,
|
||||
duration: "00:30:20",
|
||||
creatorId: 1,
|
||||
creatorNickname: "유저1",
|
||||
isPointAvailable: false,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
GetAudioContentRankingItem(
|
||||
contentId: 4,
|
||||
title: "안녕하세요 오늘은 커버곡을 들려드릴께요....안녕하세요 오늘은 커버곡을 들려드릴께요....",
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
themeStr: "커버곡",
|
||||
price: 50,
|
||||
duration: "00:30:20",
|
||||
creatorId: 1,
|
||||
creatorNickname: "유저1",
|
||||
isPointAvailable: false,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
)
|
||||
]
|
||||
) { _ in }
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
//
|
||||
// ContentCreatorView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/20/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct ContentCreatorView: View {
|
||||
|
||||
let isSelected: Bool
|
||||
let item: ContentCreatorResponse
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 13.3) {
|
||||
KFImage(URL(string: item.creatorProfileImageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(size: CGSize(width: 60, height: 60))
|
||||
.resizable()
|
||||
.frame(width: 60, height: 60)
|
||||
.clipShape(Circle())
|
||||
.overlay(
|
||||
Circle()
|
||||
.strokeBorder(lineWidth: 3)
|
||||
.foregroundColor(
|
||||
.button
|
||||
.opacity(isSelected ? 1 : 0)
|
||||
)
|
||||
)
|
||||
|
||||
Text(item.creatorNickname)
|
||||
.appFont(size: 11.3, weight: .medium)
|
||||
.foregroundColor(
|
||||
isSelected ?
|
||||
Color.button :
|
||||
Color.graybb
|
||||
|
||||
)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
.frame(width: 60)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentCreatorView(
|
||||
isSelected: true,
|
||||
item: ContentCreatorResponse(
|
||||
creatorId: 1,
|
||||
creatorNickname: "유저1",
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
//
|
||||
// ContentMainCurationViewV2.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainCurationViewV2: View {
|
||||
|
||||
let curationList: [GetContentCurationResponse]
|
||||
|
||||
var body: some View {
|
||||
LazyVStack(spacing: 30) {
|
||||
ForEach(0..<curationList.count, id: \.self) { index in
|
||||
let curation = curationList[index]
|
||||
ContentMainCurationItemViewV2(curation: curation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentMainCurationItemViewV2: View {
|
||||
let curation: GetContentCurationResponse
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text(curation.title)
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(.grayee)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 13.3) {
|
||||
ForEach(0..<curation.items.count, id: \.self) { index in
|
||||
let item = curation.items[index]
|
||||
ContentMainItemView(item: item)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainCurationViewV2(
|
||||
curationList: [
|
||||
GetContentCurationResponse(
|
||||
title: "test1",
|
||||
items: [
|
||||
GetAudioContentMainItem(
|
||||
contentId: 1,
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
title: "ㅓ처랴햐햫햐햐",
|
||||
creatorId: 8,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorNickname: "유저1",
|
||||
price: 10,
|
||||
duration: "00:00:30",
|
||||
isPointAvailable: true
|
||||
),
|
||||
GetAudioContentMainItem(
|
||||
contentId: 2,
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
title: "ㅓ처랴햐햫햐햐",
|
||||
creatorId: 8,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorNickname: "유저2",
|
||||
price: 10,
|
||||
duration: "00:00:30",
|
||||
isPointAvailable: false
|
||||
),
|
||||
GetAudioContentMainItem(
|
||||
contentId: 3,
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
title: "ㅓ처랴햐햫햐햐",
|
||||
creatorId: 8,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorNickname: "유저3",
|
||||
price: 10,
|
||||
duration: "00:00:30",
|
||||
isPointAvailable: false
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
//
|
||||
// ContentMainNewContentViewV2.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainNewContentViewV2: View {
|
||||
|
||||
let title: String
|
||||
let onClickMore: () -> Void
|
||||
let themeList: [String]
|
||||
let contentList: [GetAudioContentMainItem]
|
||||
|
||||
let selectTheme: (String) -> Void
|
||||
@State private var selectedTheme = "전체"
|
||||
|
||||
var body: some View {
|
||||
LazyVStack(spacing: 13.3) {
|
||||
HStack(spacing: 0) {
|
||||
Text(title)
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(.grayee)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("ic_forward")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
.onTapGesture { onClickMore() }
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
if !themeList.isEmpty {
|
||||
ContentMainContentThemeView(
|
||||
themeList: themeList,
|
||||
selectTheme: selectTheme,
|
||||
selectedTheme: $selectedTheme
|
||||
)
|
||||
}
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 13.3) {
|
||||
ForEach(0..<contentList.count, id: \.self) { index in
|
||||
ContentMainItemView(item: contentList[index])
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if !themeList.isEmpty {
|
||||
selectedTheme = themeList[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainNewContentViewV2(
|
||||
title: "새로운 단편",
|
||||
onClickMore: {},
|
||||
themeList: ["전체", "테스트1", "테스트2"],
|
||||
contentList: [
|
||||
GetAudioContentMainItem(
|
||||
contentId: 1,
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
title: "ㅓ처랴햐햫햐햐",
|
||||
creatorId: 8,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorNickname: "유저1",
|
||||
price: 10,
|
||||
duration: "00:00:30",
|
||||
isPointAvailable: false
|
||||
),
|
||||
GetAudioContentMainItem(
|
||||
contentId: 2,
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
title: "ㅓ처랴햐햫햐햐",
|
||||
creatorId: 8,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorNickname: "유저2",
|
||||
price: 10,
|
||||
duration: "00:00:30",
|
||||
isPointAvailable: false
|
||||
),
|
||||
GetAudioContentMainItem(
|
||||
contentId: 3,
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
title: "ㅓ처랴햐햫햐햐",
|
||||
creatorId: 8,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorNickname: "유저3",
|
||||
price: 10,
|
||||
duration: "00:00:30",
|
||||
isPointAvailable: false
|
||||
)
|
||||
],
|
||||
selectTheme: { _ in }
|
||||
)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
//
|
||||
// ContentMainNoItemView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainNoItemView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
Image("ic_no_item")
|
||||
.resizable()
|
||||
.frame(width: 60, height: 60)
|
||||
|
||||
Text("마이페이지에서 본인인증을 해주세요")
|
||||
.appFont(size: 13, weight: .medium)
|
||||
.foregroundColor(.graybb)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineSpacing(8)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.padding(.vertical, 16.7)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.bg)
|
||||
.cornerRadius(4.7)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainNoItemView()
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
//
|
||||
// ContentMainViewV2.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
enum ContentMainTab {
|
||||
case HOME
|
||||
case SERIES
|
||||
case CONTENT
|
||||
case ALARM
|
||||
case ASMR
|
||||
case REPLAY
|
||||
case FREE
|
||||
}
|
||||
|
||||
struct TabItem {
|
||||
let title: String
|
||||
let tab: ContentMainTab
|
||||
}
|
||||
|
||||
struct ContentMainViewV2: View {
|
||||
|
||||
@StateObject var contentPlayManager = ContentPlayManager.shared
|
||||
@StateObject var contentPlayerPlayManager = ContentPlayerPlayManager.shared
|
||||
|
||||
@State private var selectedTab: ContentMainTab = .SERIES
|
||||
@State private var isShowPlayer = false
|
||||
|
||||
let tabItemList = [
|
||||
TabItem(title: "홈", tab: .HOME),
|
||||
TabItem(title: "시리즈", tab: .SERIES),
|
||||
TabItem(title: "단편", tab: .CONTENT),
|
||||
TabItem(title: "모닝콜", tab: .ALARM),
|
||||
TabItem(title: "ASMR", tab: .ASMR),
|
||||
TabItem(title: "다시듣기", tab: .REPLAY),
|
||||
TabItem(title: "무료", tab: .FREE)
|
||||
]
|
||||
|
||||
init(selectedTab: ContentMainTab = .SERIES) {
|
||||
self._selectedTab = State(initialValue: selectedTab)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
ZStack {
|
||||
Color.black.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
Text("콘텐츠 마켓")
|
||||
.appFont(size: 21.3, weight: .bold)
|
||||
.foregroundColor(Color.button)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("ic_content_keep")
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(step: .myBox(currentTab: .orderlist))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(0..<tabItemList.count, id: \.self) { index in
|
||||
let tabItem = tabItemList[index]
|
||||
|
||||
Text(tabItem.title)
|
||||
.appFont(size: 16, weight: selectedTab == tabItem.tab ? .bold : .medium)
|
||||
.foregroundColor(
|
||||
selectedTab == tabItem.tab ?
|
||||
.button :
|
||||
.graybb
|
||||
)
|
||||
.padding(.horizontal, 12)
|
||||
.onTapGesture {
|
||||
if selectedTab != tabItem.tab {
|
||||
selectedTab = tabItem.tab
|
||||
proxy.scrollTo(tabItem.tab, anchor: .center)
|
||||
}
|
||||
}
|
||||
.id(tabItem.tab)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 15)
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
.onAppear {
|
||||
withAnimation {
|
||||
proxy.scrollTo(selectedTab, anchor: .center)
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedTab) { newTab in
|
||||
withAnimation {
|
||||
if newTab == .HOME {
|
||||
AppState.shared.back()
|
||||
} else {
|
||||
proxy.scrollTo(newTab, anchor: .center)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ZStack {
|
||||
switch selectedTab {
|
||||
case .HOME:
|
||||
EmptyView()
|
||||
case .SERIES:
|
||||
ContentMainTabSeriesView()
|
||||
case .CONTENT:
|
||||
ContentMainTabContentView()
|
||||
case .ALARM:
|
||||
ContentMainTabAlarmView()
|
||||
case .ASMR:
|
||||
ContentMainTabAsmrView()
|
||||
case .REPLAY:
|
||||
ContentMainTabReplayView()
|
||||
case .FREE:
|
||||
ContentMainTabFreeView()
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if contentPlayerPlayManager.isShowingMiniPlayer {
|
||||
HStack(spacing: 0) {
|
||||
KFImage(URL(string: contentPlayerPlayManager.coverImageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(
|
||||
size: CGSize(
|
||||
width: 36.7,
|
||||
height: 36.7
|
||||
)
|
||||
)
|
||||
.resizable()
|
||||
.frame(width: 36.7, height: 36.7)
|
||||
.cornerRadius(5.3)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2.3) {
|
||||
Text(contentPlayerPlayManager.title)
|
||||
.appFont(size: 13, weight: .medium)
|
||||
.foregroundColor(Color.grayee)
|
||||
.lineLimit(2)
|
||||
|
||||
Text(contentPlayerPlayManager.nickname)
|
||||
.appFont(size: 11, weight: .medium)
|
||||
.foregroundColor(Color.grayd2)
|
||||
}
|
||||
.padding(.horizontal, 10.7)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(contentPlayerPlayManager.isPlaying ? "ic_noti_pause" : "btn_bar_play")
|
||||
.resizable()
|
||||
.frame(width: 25, height: 25)
|
||||
.onTapGesture {
|
||||
contentPlayerPlayManager.playOrPause()
|
||||
}
|
||||
|
||||
Image("ic_noti_stop")
|
||||
.resizable()
|
||||
.frame(width: 25, height: 25)
|
||||
.padding(.leading, 16)
|
||||
.onTapGesture { contentPlayerPlayManager.resetPlayer() }
|
||||
}
|
||||
.padding(.vertical, 10.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
.background(Color.gray22)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
isShowPlayer = true
|
||||
}
|
||||
}
|
||||
|
||||
if contentPlayManager.isShowingMiniPlayer {
|
||||
HStack(spacing: 0) {
|
||||
KFImage(URL(string: contentPlayManager.coverImage))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(
|
||||
size: CGSize(
|
||||
width: 36.7,
|
||||
height: 36.7
|
||||
)
|
||||
)
|
||||
.resizable()
|
||||
.frame(width: 36.7, height: 36.7)
|
||||
.cornerRadius(5.3)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2.3) {
|
||||
Text(contentPlayManager.title)
|
||||
.appFont(size: 13, weight: .medium)
|
||||
.foregroundColor(Color.grayee)
|
||||
.lineLimit(2)
|
||||
|
||||
Text(contentPlayManager.nickname)
|
||||
.appFont(size: 11, weight: .medium)
|
||||
.foregroundColor(Color.grayd2)
|
||||
}
|
||||
.padding(.horizontal, 10.7)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(contentPlayManager.isPlaying ? "ic_noti_pause" : "btn_bar_play")
|
||||
.resizable()
|
||||
.frame(width: 25, height: 25)
|
||||
.onTapGesture {
|
||||
if contentPlayManager.isPlaying {
|
||||
contentPlayManager.pauseAudio()
|
||||
} else {
|
||||
contentPlayManager
|
||||
.playAudio(contentId: contentPlayManager.contentId)
|
||||
}
|
||||
}
|
||||
|
||||
Image("ic_noti_stop")
|
||||
.resizable()
|
||||
.frame(width: 25, height: 25)
|
||||
.padding(.leading, 16)
|
||||
.onTapGesture { contentPlayManager.stopAudio() }
|
||||
}
|
||||
.padding(.vertical, 10.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
.background(Color.gray22)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
AppState.shared
|
||||
.setAppStep(
|
||||
step: .contentDetail(contentId: contentPlayManager.contentId)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isShowPlayer {
|
||||
ContentPlayerView(isShowing: $isShowPlayer, playlist: [])
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainViewV2(selectedTab: .SERIES)
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
//
|
||||
// ContentMainIntroduceCreatorAllView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainIntroduceCreatorAllView: View {
|
||||
|
||||
@StateObject var viewModel = ContentMainIntroduceCreatorAllViewModel()
|
||||
@State private var isInitialized = false
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
VStack(spacing: 13.3) {
|
||||
DetailNavigationBar(title: "크리에이터 소개")
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
let horizontalPadding: CGFloat = 16
|
||||
let gridSpacing: CGFloat = 16
|
||||
let itemSize = (screenSize().width - (horizontalPadding * 2) - gridSpacing) / 2
|
||||
|
||||
LazyVGrid(
|
||||
columns: Array(
|
||||
repeating: GridItem(
|
||||
.flexible(),
|
||||
spacing: gridSpacing,
|
||||
alignment: .topLeading
|
||||
),
|
||||
count: 2
|
||||
),
|
||||
alignment: .leading,
|
||||
spacing: gridSpacing
|
||||
) {
|
||||
ForEach(0..<viewModel.introduceCreatorList.count, id: \.self) { index in
|
||||
let item = viewModel.introduceCreatorList[index]
|
||||
ContentNewAllItemView(width: itemSize, item: item)
|
||||
.onAppear {
|
||||
if index == viewModel.introduceCreatorList.count - 1 {
|
||||
viewModel.getIntroduceCreatorList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if !isInitialized {
|
||||
viewModel.getIntroduceCreatorList()
|
||||
isInitialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainIntroduceCreatorAllView()
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
//
|
||||
// ContentMainIntroduceCreatorAllViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class ContentMainIntroduceCreatorAllViewModel: ObservableObject {
|
||||
private let repository = ContentMainTabFreeRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var isLoading = false
|
||||
|
||||
@Published var introduceCreatorList: [GetAudioContentMainItem] = []
|
||||
|
||||
var page = 1
|
||||
var isLast = false
|
||||
private let size = 20
|
||||
|
||||
func getIntroduceCreatorList() {
|
||||
if (!isLast && !isLoading) {
|
||||
isLoading = true
|
||||
repository.getIntroduceCreatorList(page: page, size: size)
|
||||
.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<[GetAudioContentMainItem]>.self, from: responseData)
|
||||
self.isLoading = false
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
if page == 1 {
|
||||
introduceCreatorList.removeAll()
|
||||
}
|
||||
|
||||
if !data.isEmpty {
|
||||
page += 1
|
||||
self.introduceCreatorList.append(contentsOf: data)
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
//
|
||||
// ContentMainTabFreeRepository.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CombineMoya
|
||||
import Combine
|
||||
import Moya
|
||||
|
||||
final class ContentMainTabFreeRepository {
|
||||
|
||||
private let api = MoyaProvider<ContentApi>()
|
||||
|
||||
func getContentMainFree() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getContentMainFree(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getIntroduceCreatorList(page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getIntroduceCreatorList(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
|
||||
page: page,
|
||||
size: size
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getNewContentOfTheme(theme: String, page: Int = 1, size: Int = 20) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getNewFreeContentOfTheme(
|
||||
theme: theme,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
|
||||
page: page,
|
||||
size: size
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getPopularContentByCreator(creatorId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getPopularFreeContentByCreator(
|
||||
creatorId: creatorId,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
//
|
||||
// ContentMainTabFreeView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainTabFreeView: View {
|
||||
|
||||
@StateObject var viewModel = ContentMainTabFreeViewModel()
|
||||
|
||||
var body: some View {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(spacing: 0) {
|
||||
if !viewModel.bannerList.isEmpty {
|
||||
ContentMainBannerViewV2(bannerList: viewModel.bannerList)
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
|
||||
if let introduceCreator = viewModel.introduceCreator {
|
||||
ContentMainNewContentViewV2(
|
||||
title: introduceCreator.title,
|
||||
onClickMore: {
|
||||
AppState.shared
|
||||
.setAppStep(step: .introduceCreatorAll)
|
||||
},
|
||||
themeList: [],
|
||||
contentList: introduceCreator.items
|
||||
) { _ in }
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.recommendSeriesList.isEmpty {
|
||||
ContentMainNewOrRecommendSeriesView(
|
||||
title: "추천 무료 시리즈",
|
||||
recommendSeriesList: viewModel.recommendSeriesList
|
||||
)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.themeList.isEmpty {
|
||||
ContentMainNewContentViewV2(
|
||||
title: "새로운 무료 콘텐츠",
|
||||
onClickMore: {
|
||||
AppState.shared
|
||||
.setAppStep(step: .newContentAll(isFree: true))
|
||||
},
|
||||
themeList: viewModel.themeList,
|
||||
contentList: viewModel.newFreeContentList
|
||||
) {
|
||||
viewModel.getNewContentOfTheme(theme: $0)
|
||||
}
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.creatorList.isEmpty {
|
||||
ContentByChannelView(
|
||||
title: "채널별 추천 무료 콘텐츠",
|
||||
creatorList: viewModel.creatorList,
|
||||
contentList: viewModel.playCountRankContentList,
|
||||
onClickCreator: {
|
||||
viewModel.getPopularContentByCreator(creatorId: $0)
|
||||
}
|
||||
)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.curationList.isEmpty {
|
||||
ContentMainCurationViewV2(curationList: viewModel.curationList)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.fetchData()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainTabFreeView()
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
//
|
||||
// ContentMainTabFreeViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class ContentMainTabFreeViewModel: ObservableObject {
|
||||
private let repository = ContentMainTabFreeRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var isLoading = false
|
||||
|
||||
@Published var bannerList: [GetAudioContentBannerResponse] = []
|
||||
@Published var introduceCreator: GetContentCurationResponse? = nil
|
||||
@Published var recommendSeriesList: [GetRecommendSeriesListResponse] = []
|
||||
@Published var themeList: [String] = []
|
||||
@Published var newFreeContentList: [GetAudioContentMainItem] = []
|
||||
@Published var creatorList: [ContentCreatorResponse] = []
|
||||
@Published var playCountRankContentList: [GetAudioContentRankingItem] = []
|
||||
@Published var curationList: [GetContentCurationResponse] = []
|
||||
|
||||
func fetchData() {
|
||||
isLoading = true
|
||||
repository.getContentMainFree()
|
||||
.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<GetContentMainTabFreeResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.bannerList = data.contentBannerList
|
||||
self.introduceCreator = data.introduceCreator
|
||||
self.recommendSeriesList = data.recommendSeriesList
|
||||
self.newFreeContentList = data.newFreeContentList
|
||||
self.creatorList = data.creatorList
|
||||
self.playCountRankContentList = data.playCountRankContentList
|
||||
self.curationList = data.curationList
|
||||
|
||||
self.themeList.removeAll()
|
||||
self.themeList.append("전체")
|
||||
self.themeList.append(contentsOf: data.themeList)
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func getNewContentOfTheme(theme: String) {
|
||||
isLoading = true
|
||||
repository.getNewContentOfTheme(theme: theme)
|
||||
.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<[GetAudioContentMainItem]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.newFreeContentList = data
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func getPopularContentByCreator(creatorId: Int) {
|
||||
isLoading = true
|
||||
repository.getPopularContentByCreator(creatorId: creatorId)
|
||||
.sink { result in
|
||||
switch result {
|
||||
case .finished:
|
||||
DEBUG_LOG("finish")
|
||||
case .failure(let error):
|
||||
ERROR_LOG(error.localizedDescription)
|
||||
}
|
||||
} receiveValue: { [unowned self] response in
|
||||
let responseData = response.data
|
||||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<[GetAudioContentRankingItem]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.playCountRankContentList = data
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
//
|
||||
// GetContentMainTabFreeResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
struct GetContentMainTabFreeResponse: Decodable {
|
||||
let contentBannerList: [GetAudioContentBannerResponse]
|
||||
let introduceCreator: GetContentCurationResponse?
|
||||
let recommendSeriesList: [GetRecommendSeriesListResponse]
|
||||
let themeList: [String]
|
||||
let newFreeContentList: [GetAudioContentMainItem]
|
||||
let creatorList: [ContentCreatorResponse]
|
||||
let playCountRankContentList: [GetAudioContentRankingItem]
|
||||
let curationList: [GetContentCurationResponse]
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
//
|
||||
// GetContentCurationResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
struct GetContentCurationResponse: Decodable {
|
||||
let title: String
|
||||
let items: [GetAudioContentMainItem]
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
//
|
||||
// ContentMainTabCategoryView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/20/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainTabCategoryView: View {
|
||||
|
||||
let imageName: String
|
||||
let title: String
|
||||
let onClick: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 5.3) {
|
||||
Image(imageName)
|
||||
.resizable()
|
||||
.frame(width: 43, height: 43)
|
||||
|
||||
Text(title)
|
||||
.appFont(size: 12, weight: .medium)
|
||||
.foregroundColor(.gray77)
|
||||
}
|
||||
.onTapGesture {
|
||||
onClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainTabCategoryView(
|
||||
imageName: "ic_category_series",
|
||||
title: "시리즈",
|
||||
onClick: {}
|
||||
)
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
//
|
||||
// ContentMainTabHomeNoticeView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/20/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainTabHomeNoticeView: View {
|
||||
|
||||
let notice: NoticeItem
|
||||
let onClick: (NoticeItem) -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
Text(notice.title)
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(.white)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("자세히 >")
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(.white)
|
||||
.onTapGesture {
|
||||
onClick(notice)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
.padding(.vertical, 10)
|
||||
.background(Color.gray22)
|
||||
.cornerRadius(5.3)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainTabHomeNoticeView(
|
||||
notice: NoticeItem(
|
||||
title: "[업데이트] 1.28.0 버전 업데이트",
|
||||
content: "test",
|
||||
date: "2025-02-07"
|
||||
)
|
||||
) {
|
||||
AppState.shared.setAppStep(step: .noticeDetail(notice: $0))
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
//
|
||||
// ContentMainTabHomeRepository.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/20/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CombineMoya
|
||||
import Combine
|
||||
import Moya
|
||||
|
||||
class ContentMainTabHomeRepository {
|
||||
private let api = MoyaProvider<ContentApi>()
|
||||
|
||||
func getContentMainHome() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getContentMainHome(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getPopularContentByCreator(creatorId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getPopularContentByCreator(
|
||||
creatorId: creatorId,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getContentRanking(sortType: String = "매출") -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getContentMainHomeContentRanking(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
|
||||
sortType: sortType
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,301 +0,0 @@
|
||||
//
|
||||
// ContentMainTabHomeView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/20/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainTabHomeView: View {
|
||||
|
||||
@StateObject var viewModel = ContentMainTabHomeViewModel()
|
||||
@AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
|
||||
@AppStorage("role") private var role: String = UserDefaults.string(forKey: UserDefaultsKey.role)
|
||||
|
||||
var body: some View {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
Text("보이스온")
|
||||
.appFont(size: 21.3, weight: .bold)
|
||||
.foregroundColor(Color.white)
|
||||
.padding(.leading, 8)
|
||||
|
||||
Spacer()
|
||||
|
||||
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
Image("ic_can")
|
||||
.onTapGesture {
|
||||
AppState
|
||||
.shared
|
||||
.setAppStep(step: .canCharge(refresh: {}))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
if let notice = viewModel.noticeItem {
|
||||
ContentMainTabHomeNoticeView(notice: notice) {
|
||||
AppState.shared
|
||||
.setAppStep(step: .noticeDetail(notice: $0))
|
||||
}
|
||||
.padding(.top, 15)
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
|
||||
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
|
||||
viewModel.bannerList.count > 0 {
|
||||
ContentMainBannerViewV2(bannerList: viewModel.bannerList)
|
||||
.padding(.top, 30)
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
|
||||
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
HStack(spacing: 0) {
|
||||
Image("ic_title_search_black")
|
||||
|
||||
Text("검색어를 2글자 이상 입력하세요")
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(Color.gray55)
|
||||
.keyboardType(.default)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 21.3)
|
||||
.frame(height: 50)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.gray22)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 6.7)
|
||||
.strokeBorder(lineWidth: 1)
|
||||
.foregroundColor(Color.graybb)
|
||||
)
|
||||
.padding(.top, 30)
|
||||
.padding(.horizontal, 13.3)
|
||||
.onTapGesture {
|
||||
UserDefaults.set("", forKey: .searchChannel)
|
||||
AppState.shared.setAppStep(step: .search)
|
||||
}
|
||||
|
||||
VStack(spacing: 13.3) {
|
||||
HStack(spacing: 0) {
|
||||
ContentMainTabCategoryView(
|
||||
imageName: "ic_category_series",
|
||||
title: "시리즈",
|
||||
onClick: {
|
||||
AppState.shared
|
||||
.setAppStep(
|
||||
step: .contentMain(
|
||||
startTab: .SERIES
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
ContentMainTabCategoryView(
|
||||
imageName: "ic_category_content",
|
||||
title: "단편",
|
||||
onClick: {
|
||||
AppState.shared
|
||||
.setAppStep(
|
||||
step: .contentMain(
|
||||
startTab: .CONTENT
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
ContentMainTabCategoryView(
|
||||
imageName: "ic_category_alarm",
|
||||
title: "모닝콜",
|
||||
onClick: {
|
||||
AppState.shared
|
||||
.setAppStep(
|
||||
step: .contentMain(
|
||||
startTab: .ALARM
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
ContentMainTabCategoryView(
|
||||
imageName: "ic_category_asmr",
|
||||
title: "ASMR",
|
||||
onClick: {
|
||||
AppState.shared
|
||||
.setAppStep(
|
||||
step: .contentMain(
|
||||
startTab: .ASMR
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
HStack(spacing: 0) {
|
||||
ContentMainTabCategoryView(
|
||||
imageName: "ic_category_replay",
|
||||
title: "다시듣기",
|
||||
onClick: {
|
||||
AppState.shared
|
||||
.setAppStep(
|
||||
step: .contentMain(
|
||||
startTab: .REPLAY
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
ContentMainTabCategoryView(
|
||||
imageName: "ic_category_free",
|
||||
title: "무료",
|
||||
onClick: {
|
||||
AppState.shared
|
||||
.setAppStep(
|
||||
step: .contentMain(
|
||||
startTab: .FREE
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
ContentMainTabCategoryView(
|
||||
imageName: "ic_category_audio_book",
|
||||
title: "오디오북",
|
||||
onClick: {
|
||||
viewModel.errorMessage = "준비중입니다."
|
||||
viewModel.isShowPopup = true
|
||||
}
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
ContentMainTabCategoryView(
|
||||
imageName: "ic_category_audio_toon",
|
||||
title: "오디오툰",
|
||||
onClick: {
|
||||
viewModel.errorMessage = "준비중입니다."
|
||||
viewModel.isShowPopup = true
|
||||
}
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 13.3)
|
||||
.background(Color.gray22)
|
||||
.cornerRadius(5.3)
|
||||
.padding(.top, 30)
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
|
||||
if let response = viewModel.rankCreatorResponse {
|
||||
ContentMainTabHomeRankCreatorView(response: response)
|
||||
.padding(.top, 30)
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
|
||||
if !viewModel.rankSeriesList.isEmpty {
|
||||
ContentMainTabHomeRankSeriesView(seriesList: viewModel.rankSeriesList)
|
||||
.padding(.top, 30)
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
|
||||
if !viewModel.rankSortTypeList.isEmpty {
|
||||
ContentMainTabRankContentView(
|
||||
title: "인기 단편",
|
||||
isMore: true,
|
||||
onClickMore: {
|
||||
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
AppState.shared.setAppStep(step: .contentRankingAll)
|
||||
} else {
|
||||
AppState.shared.setAppStep(step: .login)
|
||||
}
|
||||
},
|
||||
sortList: !viewModel.rankSortTypeList.isEmpty ?
|
||||
viewModel.rankSortTypeList :
|
||||
[],
|
||||
onClickSort: { viewModel.getContentRanking(sort: $0) },
|
||||
contentList: viewModel.rankContentList
|
||||
)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
|
||||
viewModel.eventBannerList.count > 0 {
|
||||
SectionEventBannerView(items: viewModel.eventBannerList)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.contentRankCreatorList.isEmpty {
|
||||
ContentByChannelView(
|
||||
title: "채널별 인기 콘텐츠",
|
||||
creatorList: viewModel.contentRankCreatorList,
|
||||
contentList: viewModel.salesCountRankContentList,
|
||||
onClickCreator: {
|
||||
viewModel.getPopularContentByCreator(creatorId: $0)
|
||||
}
|
||||
)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
Text("""
|
||||
- 회사명 : 주식회사 소다라이브
|
||||
|
||||
- 대표자 : 이재형
|
||||
|
||||
- 주소 : 경기도 성남시 분당구 황새울로335번길 10, 5층 563A호
|
||||
|
||||
- 사업자등록번호 : 870-81-03220
|
||||
|
||||
- 통신판매업신고 : 제2024-성남분당B-1012호
|
||||
|
||||
- 고객센터 : 02.2055.1477 (이용시간 10:00~19:00)
|
||||
|
||||
- 대표 이메일 : sodalive.official@gmail.com
|
||||
""")
|
||||
.appFont(size: 11, weight: .medium)
|
||||
.foregroundColor(Color.gray77)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.fetchData()
|
||||
}
|
||||
}
|
||||
|
||||
if role == MemberRole.CREATOR.rawValue {
|
||||
HStack(spacing: 5) {
|
||||
Image("ic_thumb_play")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
|
||||
Text("콘텐츠 업로드")
|
||||
.appFont(size: 13.3, weight: .bold)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(13.3)
|
||||
.background(Color(hex: "3bb9f1"))
|
||||
.cornerRadius(44)
|
||||
.padding(.trailing, 16.7)
|
||||
.padding(.bottom, 16.7)
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(step: .createContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainTabHomeView()
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
//
|
||||
// ContentMainTabHomeViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/20/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class ContentMainTabHomeViewModel: ObservableObject {
|
||||
|
||||
private let repository = ContentMainTabHomeRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var isLoading = false
|
||||
|
||||
@Published var noticeItem: NoticeItem? = nil
|
||||
@Published var bannerList = [GetAudioContentBannerResponse]()
|
||||
@Published var rankCreatorResponse: GetExplorerSectionResponse? = nil
|
||||
@Published var rankSeriesList = [SeriesListItem]()
|
||||
@Published var rankSortTypeList: [String] = []
|
||||
@Published var rankContentList: [GetAudioContentRankingItem] = []
|
||||
@Published var eventBannerList: [EventItem] = []
|
||||
@Published var contentRankCreatorList: [ContentCreatorResponse] = []
|
||||
@Published var salesCountRankContentList: [GetAudioContentRankingItem] = []
|
||||
|
||||
func fetchData() {
|
||||
isLoading = true
|
||||
|
||||
repository.getContentMainHome()
|
||||
.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<GetContentMainTabHomeResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.noticeItem = data.latestNotice
|
||||
self.bannerList = data.bannerList
|
||||
self.rankCreatorResponse = data.rankCreatorList
|
||||
self.rankSeriesList = data.rankSeriesList
|
||||
self.rankSortTypeList = data.rankSortTypeList
|
||||
self.rankContentList = data.rankContentList
|
||||
self.eventBannerList = data.eventBannerList.eventList
|
||||
self.contentRankCreatorList = data.contentRankCreatorList
|
||||
self.salesCountRankContentList = data.salesCountRankContentList
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func getContentRanking(sort: String = "매출") {
|
||||
isLoading = true
|
||||
repository.getContentRanking(sortType: 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<[GetAudioContentRankingItem]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.rankContentList = data
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func getPopularContentByCreator(creatorId: Int) {
|
||||
isLoading = true
|
||||
repository.getPopularContentByCreator(creatorId: creatorId)
|
||||
.sink { result in
|
||||
switch result {
|
||||
case .finished:
|
||||
DEBUG_LOG("finish")
|
||||
case .failure(let error):
|
||||
ERROR_LOG(error.localizedDescription)
|
||||
}
|
||||
} receiveValue: { [unowned self] response in
|
||||
let responseData = response.data
|
||||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<[GetAudioContentRankingItem]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.salesCountRankContentList = data
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// GetContentMainTabHomeResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/20/25.
|
||||
//
|
||||
|
||||
struct GetContentMainTabHomeResponse: Decodable {
|
||||
let latestNotice: NoticeItem?
|
||||
let bannerList: [GetAudioContentBannerResponse]
|
||||
let rankCreatorList: GetExplorerSectionResponse
|
||||
let rankSeriesList: [SeriesListItem]
|
||||
let rankSortTypeList: [String]
|
||||
let rankContentList: [GetAudioContentRankingItem]
|
||||
let eventBannerList: GetEventResponse
|
||||
let contentRankCreatorList: [ContentCreatorResponse]
|
||||
let salesCountRankContentList: [GetAudioContentRankingItem]
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
//
|
||||
// ContentMainTabHomeRankCreatorView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/20/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct ContentMainTabHomeRankCreatorView: View {
|
||||
|
||||
@AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
|
||||
|
||||
let response: 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: 0) {
|
||||
if let desc = response.desc, !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
VStack(spacing: 8) {
|
||||
Text("\(desc)")
|
||||
.appFont(size: 14.7, weight: .bold)
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
Text("※ 인기 순위는 매주 업데이트됩니다.")
|
||||
.appFont(size: 13.3, weight: .light)
|
||||
.foregroundColor(Color.graybb)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.gray22)
|
||||
}
|
||||
|
||||
if let coloredTitle = response.coloredTitle, let color = response.color {
|
||||
let titleArray = response.title.components(separatedBy: coloredTitle)
|
||||
HStack(spacing: 0) {
|
||||
Text(titleArray[0])
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
Text(coloredTitle)
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(Color(hex: color))
|
||||
|
||||
if titleArray.count > 1 {
|
||||
Text(titleArray[1])
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(Color.grayee)
|
||||
}
|
||||
}
|
||||
.padding(.top, token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? 0 : 30)
|
||||
} else {
|
||||
Text(response.title)
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(Color.grayee)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 13.3) {
|
||||
ForEach(0..<response.creators.count, id: \.self) { index in
|
||||
let creator = response.creators[index]
|
||||
VStack(spacing: 0) {
|
||||
if let _ = response.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)
|
||||
.appFont(size: 11.3, weight: .medium)
|
||||
.foregroundColor(Color.grayee)
|
||||
.lineLimit(1)
|
||||
.frame(width: 93.3)
|
||||
.padding(.top, 13.3)
|
||||
|
||||
Text(creator.tags)
|
||||
.appFont(size: 10, weight: .medium)
|
||||
.foregroundColor(Color.button)
|
||||
.lineLimit(1)
|
||||
.frame(width: 93.3)
|
||||
.padding(.top, 3.3)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
AppState.shared
|
||||
.setAppStep(step: .creatorDetail(userId: creator.id))
|
||||
} else {
|
||||
AppState.shared
|
||||
.setAppStep(step: .login)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 13.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainTabHomeRankCreatorView(
|
||||
response: GetExplorerSectionResponse(
|
||||
title: "인기 크리에이터",
|
||||
coloredTitle: "인기",
|
||||
color: "ff5c49",
|
||||
desc: "2025년 02월 10일 ~ 02월 16일",
|
||||
creators: [
|
||||
GetExplorerSectionCreatorResponse(
|
||||
id: 1,
|
||||
nickname: "User1",
|
||||
tags: "",
|
||||
profileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
follow: false
|
||||
),
|
||||
GetExplorerSectionCreatorResponse(
|
||||
id: 2,
|
||||
nickname: "User2",
|
||||
tags: "",
|
||||
profileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
follow: false
|
||||
),
|
||||
GetExplorerSectionCreatorResponse(
|
||||
id: 3,
|
||||
nickname: "User3",
|
||||
tags: "",
|
||||
profileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
follow: false
|
||||
),
|
||||
GetExplorerSectionCreatorResponse(
|
||||
id: 4,
|
||||
nickname: "User4",
|
||||
tags: "",
|
||||
profileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
follow: false
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
//
|
||||
// ContentMainTabHomeRankSeriesView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/20/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainTabHomeRankSeriesView: View {
|
||||
|
||||
let seriesList: [SeriesListItem]
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text("인기 시리즈")
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(Color.grayee)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(alignment: .top, spacing: 13.3) {
|
||||
ForEach(0..<seriesList.count, id: \.self) {
|
||||
let item = seriesList[$0]
|
||||
SeriesListBigItemView(item: item, isVisibleCreator: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainTabHomeRankSeriesView(
|
||||
seriesList: [
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 2,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: false,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: false,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: false,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: false
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
//
|
||||
// ContentMainReplayAllView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainReplayAllView: View {
|
||||
|
||||
@StateObject var viewModel = ContentNewAllViewModel()
|
||||
@State private var isInitialized = false
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
DetailNavigationBar(title: "새로운 라이브 다시듣기")
|
||||
|
||||
Text("※ 최근 2주간 등록된 새로운 라이브 다시듣기 입니다.")
|
||||
.appFont(size: 14.7, weight: .medium)
|
||||
.foregroundColor(.graybb)
|
||||
.padding(.horizontal, 13.3)
|
||||
.padding(.vertical, 8)
|
||||
.frame(width: screenSize().width, alignment: .leading)
|
||||
.background(Color.gray22)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Text("전체")
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(Color(hex: "e2e2e2"))
|
||||
|
||||
Text("\(viewModel.totalCount)")
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(Color(hex: "ff5c49"))
|
||||
.padding(.leading, 8)
|
||||
|
||||
Text("개")
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(Color(hex: "e2e2e2"))
|
||||
.padding(.leading, 2)
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
let horizontalPadding: CGFloat = 16
|
||||
let gridSpacing: CGFloat = 16
|
||||
let itemSize = (screenSize().width - (horizontalPadding * 2) - gridSpacing) / 2
|
||||
|
||||
LazyVGrid(
|
||||
columns: Array(
|
||||
repeating: GridItem(
|
||||
.flexible(),
|
||||
spacing: gridSpacing,
|
||||
alignment: .topLeading
|
||||
),
|
||||
count: 2
|
||||
),
|
||||
alignment: .leading,
|
||||
spacing: gridSpacing
|
||||
) {
|
||||
ForEach(0..<viewModel.newContentList.count, id: \.self) { index in
|
||||
ContentNewAllItemView(width: itemSize, item: viewModel.newContentList[index])
|
||||
.onAppear {
|
||||
if index == viewModel.newContentList.count - 1 {
|
||||
viewModel.getNewContentList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if !isInitialized {
|
||||
if viewModel.selectedTheme != "다시듣기" {
|
||||
viewModel.selectedTheme = "다시듣기"
|
||||
} else if viewModel.newContentList.isEmpty {
|
||||
viewModel.getNewContentList()
|
||||
}
|
||||
isInitialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainReplayAllView()
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
//
|
||||
// ContentMainTabReplayRepository.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CombineMoya
|
||||
import Combine
|
||||
import Moya
|
||||
|
||||
final class ContentMainTabReplayRepository {
|
||||
|
||||
private let api = MoyaProvider<ContentApi>()
|
||||
|
||||
func getContentMainReplay() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getContentMainReplay(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getPopularContentByCreator(creatorId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getPopularReplayContentByCreator(
|
||||
creatorId: creatorId,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
//
|
||||
// ContentMainTabReplayView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainTabReplayView: View {
|
||||
|
||||
@StateObject var viewModel = ContentMainTabReplayViewModel()
|
||||
|
||||
var body: some View {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(spacing: 0) {
|
||||
if !viewModel.bannerList.isEmpty {
|
||||
ContentMainBannerViewV2(bannerList: viewModel.bannerList)
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
|
||||
if !viewModel.newReplayContentList.isEmpty {
|
||||
ContentMainNewContentViewV2(
|
||||
title: "새로운 라이브 다시듣기",
|
||||
onClickMore: {
|
||||
AppState.shared
|
||||
.setAppStep(step: .newReplayContentAll)
|
||||
},
|
||||
themeList: [],
|
||||
contentList: viewModel.newReplayContentList
|
||||
) { _ in }
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.creatorList.isEmpty {
|
||||
ContentByChannelView(
|
||||
title: "채널별 라이브 다시듣기",
|
||||
creatorList: viewModel.creatorList,
|
||||
contentList: viewModel.salesCountRankContentList,
|
||||
onClickCreator: {
|
||||
viewModel.getPopularContentByCreator(creatorId: $0)
|
||||
}
|
||||
)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.eventBannerList.isEmpty {
|
||||
SectionEventBannerView(items: viewModel.eventBannerList)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.curationList.isEmpty {
|
||||
ContentMainCurationViewV2(curationList: viewModel.curationList)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.fetchData()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainTabReplayView()
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
//
|
||||
// ContentMainTabReplayViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class ContentMainTabReplayViewModel: ObservableObject {
|
||||
private let repository = ContentMainTabReplayRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var isLoading = false
|
||||
|
||||
@Published var bannerList: [GetAudioContentBannerResponse] = []
|
||||
@Published var newReplayContentList: [GetAudioContentMainItem] = []
|
||||
@Published var creatorList: [ContentCreatorResponse] = []
|
||||
@Published var salesCountRankContentList: [GetAudioContentRankingItem] = []
|
||||
@Published var eventBannerList: [EventItem] = []
|
||||
@Published var curationList: [GetContentCurationResponse] = []
|
||||
|
||||
func fetchData() {
|
||||
isLoading = true
|
||||
repository.getContentMainReplay()
|
||||
.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<GetContentMainTabReplayResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.bannerList = data.contentBannerList
|
||||
self.newReplayContentList = data.newLiveReplayContentList
|
||||
self.creatorList = data.creatorList
|
||||
self.salesCountRankContentList = data.salesCountRankContentList
|
||||
self.eventBannerList = data.eventBannerList.eventList
|
||||
self.curationList = data.curationList
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func getPopularContentByCreator(creatorId: Int) {
|
||||
isLoading = true
|
||||
repository.getPopularContentByCreator(creatorId: creatorId)
|
||||
.sink { result in
|
||||
switch result {
|
||||
case .finished:
|
||||
DEBUG_LOG("finish")
|
||||
case .failure(let error):
|
||||
ERROR_LOG(error.localizedDescription)
|
||||
}
|
||||
} receiveValue: { [unowned self] response in
|
||||
let responseData = response.data
|
||||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<[GetAudioContentRankingItem]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.salesCountRankContentList = data
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
//
|
||||
// GetContentMainTabReplayResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
struct GetContentMainTabReplayResponse: Decodable {
|
||||
let contentBannerList: [GetAudioContentBannerResponse]
|
||||
let newLiveReplayContentList: [GetAudioContentMainItem]
|
||||
let creatorList: [ContentCreatorResponse]
|
||||
let salesCountRankContentList: [GetAudioContentRankingItem]
|
||||
let eventBannerList: GetEventResponse
|
||||
let curationList: [GetContentCurationResponse]
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
//
|
||||
// CompletedSeriesView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CompletedSeriesView: View {
|
||||
|
||||
@StateObject var viewModel = CompletedSeriesViewModel()
|
||||
|
||||
private let columns = [
|
||||
GridItem(.flexible()),
|
||||
GridItem(.flexible()),
|
||||
GridItem(.flexible())
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
VStack(spacing: 13.3) {
|
||||
DetailNavigationBar(title: "완결 시리즈")
|
||||
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
Text("전체")
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(.graye2)
|
||||
|
||||
Text("\(viewModel.totalCount)")
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(.mainRed)
|
||||
.padding(.leading, 6.7)
|
||||
|
||||
Text("개")
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(.graye2)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
LazyVGrid(columns: columns, spacing: 13.3) {
|
||||
ForEach(0..<viewModel.rankCompleteSeriesList.count, id: \.self) { index in
|
||||
let item = viewModel.rankCompleteSeriesList[index]
|
||||
|
||||
SeriesListItemView(
|
||||
itemWidth: (screenSize().width - (13.3 * 4)) / 3,
|
||||
item: item
|
||||
)
|
||||
.onAppear {
|
||||
if index == viewModel.rankCompleteSeriesList.count - 1 {
|
||||
viewModel.getCompletedSeries()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
.onAppear {
|
||||
viewModel.getCompletedSeries()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CompletedSeriesView()
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
//
|
||||
// CompletedSeriesViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class CompletedSeriesViewModel: ObservableObject {
|
||||
private let repository = ContentMainTabSeriesRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var isLoading = false
|
||||
|
||||
@Published var rankCompleteSeriesList: [SeriesListItem] = []
|
||||
@Published var totalCount = 0
|
||||
|
||||
var isLast = false
|
||||
var page = 1
|
||||
private let size = 20
|
||||
|
||||
func getCompletedSeries() {
|
||||
if !isLast && !isLoading {
|
||||
isLoading = true
|
||||
|
||||
repository.getCompletedSeries(page: page, size: size)
|
||||
.sink { result in
|
||||
switch result {
|
||||
case .finished:
|
||||
DEBUG_LOG("finish")
|
||||
case .failure(let error):
|
||||
ERROR_LOG(error.localizedDescription)
|
||||
}
|
||||
} receiveValue: { [unowned self] response in
|
||||
let responseData = response.data
|
||||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<GetSeriesListResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
page += 1
|
||||
|
||||
if (data.items.count > 0) {
|
||||
self.totalCount = data.totalCount
|
||||
self.rankCompleteSeriesList = 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
//
|
||||
// ContentMainCompletedSeriesView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainCompletedSeriesView: View {
|
||||
|
||||
let itemList: [SeriesListItem]
|
||||
let onClickMore: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 13.3) {
|
||||
HStack(spacing: 0) {
|
||||
Text("완결 시리즈")
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(.grayee)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("ic_forward")
|
||||
.onTapGesture {
|
||||
onClickMore()
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 13.3) {
|
||||
ForEach(0..<itemList.count, id: \.self) { index in
|
||||
let item = itemList[index]
|
||||
SeriesListBigItemView(
|
||||
item: item,
|
||||
isVisibleCreator: true
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainCompletedSeriesView(
|
||||
itemList: [
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
)
|
||||
],
|
||||
onClickMore: {}
|
||||
)
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
//
|
||||
// ContentMainNewOrRecommendSeriesView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct ContentMainNewOrRecommendSeriesView: View {
|
||||
|
||||
let title: String
|
||||
let recommendSeriesList: [GetRecommendSeriesListResponse]
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text(title)
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 13.3) {
|
||||
ForEach(0..<recommendSeriesList.count, id: \.self) { index in
|
||||
let item = recommendSeriesList[index]
|
||||
ContentMainNewOrRecommendSeriesItemView(item: item)
|
||||
.onTapGesture {
|
||||
AppState.shared
|
||||
.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentMainNewOrRecommendSeriesItemView: View {
|
||||
|
||||
let item: GetRecommendSeriesListResponse
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 9) {
|
||||
KFImage(URL(string: item.imageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(size: CGSize(width: 267, height: 141.3))
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 267, height: 141.3)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 5))
|
||||
|
||||
Text(item.title)
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(.grayee)
|
||||
|
||||
HStack(spacing: 5.3) {
|
||||
KFImage(URL(string: item.creatorProfileImageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(size: CGSize(width: 30, height: 30))
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 30, height: 30)
|
||||
.clipShape(Circle())
|
||||
|
||||
Text(item.creatorNickname)
|
||||
.appFont(size: 10, weight: .medium)
|
||||
.foregroundColor(.gray77)
|
||||
}
|
||||
.onTapGesture {
|
||||
AppState.shared
|
||||
.setAppStep(step: .creatorDetail(userId: item.creatorId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainNewOrRecommendSeriesView(
|
||||
title: "추천 무료 시리즈",
|
||||
recommendSeriesList: [
|
||||
GetRecommendSeriesListResponse(
|
||||
seriesId: 1,
|
||||
title: "시리즈 1",
|
||||
imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorId: 1,
|
||||
creatorNickname: "유저1",
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
GetRecommendSeriesListResponse(
|
||||
seriesId: 1,
|
||||
title: "시리즈 1",
|
||||
imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorId: 1,
|
||||
creatorNickname: "유저1",
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
contentId: 1,
|
||||
title: "안녕하세요 오늘은 커버곡을 들려드릴께요....안녕하세요 오늘은 커버곡을 들려드릴께요....",
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
themeStr: "커버곡",
|
||||
price: 100,
|
||||
duration: "00:30:20",
|
||||
creatorId: 1,
|
||||
creatorNickname: "유저1",
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
|
||||
|
||||
*/
|
||||
@@ -1,116 +0,0 @@
|
||||
//
|
||||
// ContentMainSeriesByGenreView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainSeriesByGenreView: View {
|
||||
|
||||
let genreList: [GetSeriesGenreListResponse]
|
||||
let itemList: [SeriesListItem]
|
||||
let onClickGenre: (Int) -> Void
|
||||
|
||||
@State private var selectedGenreId = 0
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text("장르별 추천 시리즈")
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(Color.grayee)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ContentMainSeriesGenreView(
|
||||
genreList: genreList,
|
||||
selectGenre: {
|
||||
selectedGenreId = $0
|
||||
onClickGenre($0)
|
||||
},
|
||||
selectedGenreId: $selectedGenreId
|
||||
)
|
||||
|
||||
if !itemList.isEmpty {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 13.3) {
|
||||
ForEach(0..<itemList.count, id: \.self) { index in
|
||||
let item = itemList[index]
|
||||
|
||||
SeriesListBigItemView(
|
||||
item: item,
|
||||
isVisibleCreator: true
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
} else {
|
||||
ContentMainNoItemView()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if !genreList.isEmpty {
|
||||
selectedGenreId = genreList[0].id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainSeriesByGenreView(
|
||||
genreList: [
|
||||
GetSeriesGenreListResponse(id: 1, genre: "test"),
|
||||
GetSeriesGenreListResponse(id: 2, genre: "test2"),
|
||||
GetSeriesGenreListResponse(id: 3, genre: "test3")
|
||||
],
|
||||
itemList: [
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: false,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 2,
|
||||
title: "제목2",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "랜덤",
|
||||
isComplete: false,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: false
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 2,
|
||||
title: "제목2",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "랜덤",
|
||||
isComplete: false,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: false
|
||||
)
|
||||
],
|
||||
onClickGenre: { _ in }
|
||||
)
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
//
|
||||
// ContentMainSeriesCurationView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainSeriesCurationView: View {
|
||||
|
||||
let curationList: [GetSeriesCurationResponse]
|
||||
|
||||
var body: some View {
|
||||
LazyVStack(spacing: 30) {
|
||||
ForEach(0..<curationList.count, id: \.self) { index in
|
||||
let curation = curationList[index]
|
||||
ContentMainSeriesCurationItemView(curation: curation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentMainSeriesCurationItemView: View {
|
||||
|
||||
let curation: GetSeriesCurationResponse
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text(curation.title)
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(.grayee)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 13.3) {
|
||||
ForEach(0..<curation.items.count, id: \.self) { index in
|
||||
let item = curation.items[index]
|
||||
SeriesListBigItemView(item: item, isVisibleCreator: true)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainSeriesCurationView(
|
||||
curationList: [
|
||||
GetSeriesCurationResponse(
|
||||
title: "test",
|
||||
items: [
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
)
|
||||
]
|
||||
),
|
||||
GetSeriesCurationResponse(
|
||||
title: "test2",
|
||||
items: [
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
)
|
||||
]
|
||||
),
|
||||
GetSeriesCurationResponse(
|
||||
title: "test3",
|
||||
items: [
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
//
|
||||
// ContentMainSeriesGenreView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainSeriesGenreView: View {
|
||||
let genreList: [GetSeriesGenreListResponse]
|
||||
let selectGenre: (Int) -> Void
|
||||
|
||||
@Binding var selectedGenreId: Int
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
ForEach(0..<genreList.count, id: \.self) { index in
|
||||
let genre = genreList[index]
|
||||
Text(genre.genre)
|
||||
.appFont(size: 14.7, weight: .medium)
|
||||
.foregroundColor(selectedGenreId == genre.id ? Color.button : Color.gray77)
|
||||
.padding(.horizontal, 13.3)
|
||||
.padding(.vertical, 9.3)
|
||||
.border(
|
||||
selectedGenreId == genre.id ? Color.button : Color.grayee,
|
||||
width: 1
|
||||
)
|
||||
.cornerRadius(16.7)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: CGFloat(16.7))
|
||||
.stroke(lineWidth: 1)
|
||||
.foregroundColor(selectedGenreId == genre.id ? Color.button : Color.grayee)
|
||||
)
|
||||
.onTapGesture {
|
||||
if selectedGenreId != genre.id {
|
||||
selectGenre(genre.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainSeriesGenreView(
|
||||
genreList: [
|
||||
GetSeriesGenreListResponse(id: 1, genre: "test"),
|
||||
GetSeriesGenreListResponse(id: 2, genre: "test2"),
|
||||
GetSeriesGenreListResponse(id: 3, genre: "test3")
|
||||
],
|
||||
selectGenre: { _ in },
|
||||
selectedGenreId: .constant(2)
|
||||
)
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
//
|
||||
// ContentMainSeriesRankingView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct ContentMainSeriesRankingView: View {
|
||||
|
||||
let seriesList: [SeriesListItem]
|
||||
|
||||
let rows = [
|
||||
GridItem(.fixed(85), alignment: .leading),
|
||||
GridItem(.fixed(85), alignment: .leading),
|
||||
GridItem(.fixed(85), alignment: .leading)
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text("일간 랭킹")
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(.grayee)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHGrid(rows: rows, spacing: 13.3) {
|
||||
ForEach(0..<seriesList.count, id: \.self) { index in
|
||||
let series = seriesList[index]
|
||||
HStack(spacing: 0) {
|
||||
KFImage(URL(string: series.coverImage))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(
|
||||
size: CGSize(
|
||||
width: 60,
|
||||
height: 85
|
||||
)
|
||||
)
|
||||
.resizable()
|
||||
.frame(width: 60, height: 85)
|
||||
.cornerRadius(2.7)
|
||||
|
||||
Text("\(index + 1)")
|
||||
.appFont(size: 16.7, weight: .bold)
|
||||
.foregroundColor(.button)
|
||||
.padding(.horizontal, 12)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(series.title)
|
||||
.lineLimit(2)
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(.grayd2)
|
||||
|
||||
Text(series.creator.nickname)
|
||||
.appFont(size: 11, weight: .medium)
|
||||
.foregroundColor(.gray77)
|
||||
}
|
||||
}
|
||||
.frame(width: screenSize().width * 0.66, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
AppState
|
||||
.shared
|
||||
.setAppStep(step: .seriesDetail(seriesId: series.seriesId))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainSeriesRankingView(
|
||||
seriesList: [
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 3,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 4,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
//
|
||||
// ContentMainTabSeriesRepository.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/20/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CombineMoya
|
||||
import Combine
|
||||
import Moya
|
||||
|
||||
final class ContentMainTabSeriesRepository {
|
||||
private let api = MoyaProvider<ContentApi>()
|
||||
|
||||
func getContentMainSeries() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getContentMainSeries(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getRecommendSeriesListByGenre(genreId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getRecommendSeriesListByGenre(
|
||||
genreId: genreId,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getRecommendSeriesByCreator(creatorId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getRecommendSeriesByCreator(
|
||||
creatorId: creatorId,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getCompletedSeries(page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getCompletedSeries(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
|
||||
page: page,
|
||||
size: size
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
//
|
||||
// ContentMainTabSeriesView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/20/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainTabSeriesView: View {
|
||||
|
||||
@StateObject var viewModel = ContentMainTabSeriesViewModel()
|
||||
|
||||
var body: some View {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(spacing: 0) {
|
||||
if !viewModel.bannerList.isEmpty {
|
||||
ContentMainBannerViewV2(bannerList: viewModel.bannerList)
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
|
||||
if !viewModel.originalAudioDramaList.isEmpty {
|
||||
ContentMainOriginalAudioDramaView(itemList: viewModel.originalAudioDramaList) {
|
||||
}
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.rankSeriesList.isEmpty {
|
||||
ContentMainSeriesRankingView(seriesList: viewModel.rankSeriesList)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.genreList.isEmpty {
|
||||
ContentMainSeriesByGenreView(
|
||||
genreList: viewModel.genreList,
|
||||
itemList: viewModel.recommendSeriesList
|
||||
) {
|
||||
viewModel.getRecommendSeriesListByGenre(genreId: $0)
|
||||
}
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.newSeriesList.isEmpty {
|
||||
ContentMainNewOrRecommendSeriesView(
|
||||
title: "새로운 시리즈",
|
||||
recommendSeriesList: viewModel.newSeriesList
|
||||
)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.rankCompleteSeriesList.isEmpty {
|
||||
ContentMainCompletedSeriesView(
|
||||
itemList: viewModel.rankCompleteSeriesList,
|
||||
onClickMore: {
|
||||
AppState.shared
|
||||
.setAppStep(step: .completedSeriesAll)
|
||||
}
|
||||
)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.seriesRankCreatorList.isEmpty {
|
||||
SeriesByChannelView(
|
||||
title: "채널별 추천 시리즈",
|
||||
creatorList: viewModel.seriesRankCreatorList,
|
||||
seriesList: viewModel.recommendSeriesByChannel
|
||||
) {
|
||||
viewModel.getRecommendSeriesByCreator(creatorId: $0)
|
||||
}
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.eventBannerList.isEmpty {
|
||||
SectionEventBannerView(items: viewModel.eventBannerList)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.curationList.isEmpty {
|
||||
ContentMainSeriesCurationView(curationList: viewModel.curationList)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.fetchData()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainTabSeriesView()
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
//
|
||||
// ContentMainTabSeriesViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/20/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class ContentMainTabSeriesViewModel: ObservableObject {
|
||||
private let repository = ContentMainTabSeriesRepository()
|
||||
private let contentRepository = ContentRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var isLoading = false
|
||||
|
||||
@Published var bannerList: [GetAudioContentBannerResponse] = []
|
||||
@Published var originalAudioDramaList: [SeriesListItem] = []
|
||||
@Published var rankSeriesList: [SeriesListItem] = []
|
||||
@Published var genreList: [GetSeriesGenreListResponse] = []
|
||||
@Published var recommendSeriesList: [SeriesListItem] = []
|
||||
@Published var newSeriesList: [GetRecommendSeriesListResponse] = []
|
||||
@Published var rankCompleteSeriesList: [SeriesListItem] = []
|
||||
@Published var seriesRankCreatorList: [ContentCreatorResponse] = []
|
||||
@Published var recommendSeriesByChannel: [SeriesListItem] = []
|
||||
@Published var eventBannerList: [EventItem] = []
|
||||
@Published var curationList: [GetSeriesCurationResponse] = []
|
||||
|
||||
func fetchData() {
|
||||
isLoading = true
|
||||
|
||||
repository.getContentMainSeries()
|
||||
.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<GetContentMainTabSeriesResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.bannerList = data.contentBannerList
|
||||
self.originalAudioDramaList = data.originalAudioDrama
|
||||
self.rankSeriesList = data.rankSeriesList
|
||||
self.genreList = data.genreList
|
||||
self.recommendSeriesList = data.recommendSeriesList
|
||||
self.newSeriesList = data.newSeriesList
|
||||
self.rankCompleteSeriesList = data.rankCompleteSeriesList
|
||||
self.seriesRankCreatorList = data.seriesRankCreatorList
|
||||
self.recommendSeriesByChannel = data.recommendSeriesByChannel
|
||||
self.eventBannerList = data.eventBannerList.eventList
|
||||
self.curationList = data.curationList
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func getRecommendSeriesListByGenre(genreId: Int) {
|
||||
isLoading = true
|
||||
repository.getRecommendSeriesListByGenre(genreId: genreId)
|
||||
.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<[SeriesListItem]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.recommendSeriesList = data
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func getRecommendSeriesByCreator(creatorId: Int) {
|
||||
recommendSeriesByChannel = []
|
||||
isLoading = true
|
||||
repository.getRecommendSeriesByCreator(creatorId: creatorId)
|
||||
.sink { result in
|
||||
switch result {
|
||||
case .finished:
|
||||
DEBUG_LOG("finish")
|
||||
case .failure(let error):
|
||||
ERROR_LOG(error.localizedDescription)
|
||||
}
|
||||
} receiveValue: { [unowned self] response in
|
||||
let responseData = response.data
|
||||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<[SeriesListItem]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.recommendSeriesByChannel = data
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
}
|
||||
@@ -5,35 +5,7 @@
|
||||
// Created by klaus on 2/20/25.
|
||||
//
|
||||
|
||||
struct GetContentMainTabSeriesResponse: Decodable {
|
||||
let contentBannerList: [GetAudioContentBannerResponse]
|
||||
let originalAudioDrama: [SeriesListItem]
|
||||
let rankSeriesList: [SeriesListItem]
|
||||
let genreList: [GetSeriesGenreListResponse]
|
||||
let recommendSeriesList: [SeriesListItem]
|
||||
let newSeriesList: [GetRecommendSeriesListResponse]
|
||||
let rankCompleteSeriesList: [SeriesListItem]
|
||||
let seriesRankCreatorList: [ContentCreatorResponse]
|
||||
let recommendSeriesByChannel: [SeriesListItem]
|
||||
let eventBannerList: GetEventResponse
|
||||
let curationList: [GetSeriesCurationResponse]
|
||||
}
|
||||
|
||||
struct GetSeriesGenreListResponse: Decodable {
|
||||
let id: Int
|
||||
let genre: String
|
||||
}
|
||||
|
||||
struct GetRecommendSeriesListResponse: Decodable {
|
||||
let seriesId: Int
|
||||
let title: String
|
||||
let imageUrl: String
|
||||
let creatorId: Int
|
||||
let creatorNickname: String
|
||||
let creatorProfileImageUrl: String
|
||||
}
|
||||
|
||||
struct GetSeriesCurationResponse: Decodable {
|
||||
let title: String
|
||||
let items: [SeriesListItem]
|
||||
}
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
//
|
||||
// ContentMainOriginalAudioDramaItemView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct ContentMainOriginalAudioDramaItemView: View {
|
||||
|
||||
let itemWidth: CGFloat
|
||||
let item: SeriesListItem
|
||||
let isAll: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ZStack {
|
||||
KFImage(URL(string: item.coverImage))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(
|
||||
size: CGSize(
|
||||
width: itemWidth,
|
||||
height: (itemWidth * 636) / 450
|
||||
)
|
||||
)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: itemWidth, height: (itemWidth * 636) / 450, alignment: .center)
|
||||
.cornerRadius(5)
|
||||
.clipped()
|
||||
.onTapGesture {
|
||||
AppState.shared
|
||||
.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(spacing: 3.3) {
|
||||
if !item.isComplete && item.isNew {
|
||||
SeriesItemBadgeView(title: "신작", backgroundColor: .button)
|
||||
}
|
||||
|
||||
if item.isComplete {
|
||||
SeriesItemBadgeView(title: "완결", backgroundColor: Color(hex: "002abd"))
|
||||
}
|
||||
|
||||
if item.isPopular {
|
||||
SeriesItemBadgeView(title: "인기", backgroundColor: Color(hex: "ec6033"))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if !isAll {
|
||||
SeriesItemBadgeView(title: "총 \(item.numberOfContent)화", backgroundColor: Color.gray33.opacity(0.7))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
if isAll {
|
||||
SeriesItemBadgeView(title: "총 \(item.numberOfContent)화", backgroundColor: Color.gray33.opacity(0.7))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(3.3)
|
||||
}
|
||||
.frame(width: itemWidth, height: (itemWidth * 636) / 450, alignment: .center)
|
||||
|
||||
Text(item.title)
|
||||
.appFont(size: 12, weight: .medium)
|
||||
.foregroundColor(Color.grayee)
|
||||
.lineLimit(1)
|
||||
|
||||
if isAll {
|
||||
Text(item.publishedDaysOfWeek)
|
||||
.appFont(size: 11, weight: .medium)
|
||||
.foregroundColor(Color.gray77)
|
||||
}
|
||||
}
|
||||
.frame(width: itemWidth)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainOriginalAudioDramaItemView(
|
||||
itemWidth: 150,
|
||||
item: SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: false,
|
||||
isPopular: true
|
||||
),
|
||||
isAll: false
|
||||
)
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
//
|
||||
// ContentMainOriginalAudioDramaView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainOriginalAudioDramaView: View {
|
||||
|
||||
let itemList: [SeriesListItem]
|
||||
let onClickMore: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 13.3) {
|
||||
HStack(spacing: 0) {
|
||||
Text("오리지널 오디오 드라마")
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(.grayee)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("ic_forward")
|
||||
.onTapGesture { onClickMore() }
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 13.3) {
|
||||
ForEach(0..<itemList.count, id: \.self) { index in
|
||||
let item = itemList[index]
|
||||
|
||||
ContentMainOriginalAudioDramaItemView(
|
||||
itemWidth: 150,
|
||||
item: item,
|
||||
isAll: false
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainOriginalAudioDramaView(
|
||||
itemList: [
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: false,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 2,
|
||||
title: "제목2",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "랜덤",
|
||||
isComplete: false,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: false
|
||||
)
|
||||
]
|
||||
) {}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
//
|
||||
// SeriesByChannelView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SeriesByChannelView: View {
|
||||
|
||||
let title: String
|
||||
let creatorList: [ContentCreatorResponse]
|
||||
let seriesList: [SeriesListItem]
|
||||
let onClickCreator: (Int) -> Void
|
||||
|
||||
@State private var selectedCreatorId = 0
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text(title)
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollView(.horizontal) {
|
||||
HStack(spacing: 22) {
|
||||
ForEach(0..<creatorList.count, id: \.self) { index in
|
||||
let item = creatorList[index]
|
||||
|
||||
ContentCreatorView(
|
||||
isSelected: item.creatorId == selectedCreatorId,
|
||||
item: item
|
||||
)
|
||||
.onTapGesture {
|
||||
let creatorId = item.creatorId
|
||||
|
||||
if creatorId != selectedCreatorId {
|
||||
selectedCreatorId = creatorId
|
||||
onClickCreator(creatorId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
|
||||
if seriesList.isEmpty {
|
||||
ContentMainNoItemView()
|
||||
} else {
|
||||
ScrollView(.horizontal) {
|
||||
HStack(spacing: 13.3) {
|
||||
ForEach(0..<seriesList.count, id: \.self) { index in
|
||||
let item = seriesList[index]
|
||||
|
||||
SeriesListBigItemView(
|
||||
item: item,
|
||||
isVisibleCreator: true
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if !self.creatorList.isEmpty {
|
||||
selectedCreatorId = creatorList[0].creatorId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SeriesByChannelView(
|
||||
title: "채널별 추천 시리즈",
|
||||
creatorList: [
|
||||
ContentCreatorResponse(
|
||||
creatorId: 1,
|
||||
creatorNickname: "유저1",
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
ContentCreatorResponse(
|
||||
creatorId: 2,
|
||||
creatorNickname: "유저2",
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
)
|
||||
],
|
||||
seriesList: [
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
)
|
||||
],
|
||||
onClickCreator: { _ in }
|
||||
)
|
||||
}
|
||||
@@ -9,11 +9,11 @@ import Foundation
|
||||
import Moya
|
||||
|
||||
enum SeriesMainApi {
|
||||
case fetchHome(isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getRecommendSeriesList(isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getDayOfWeekSeriesList(dayOfWeek: SeriesPublishedDaysOfWeek, isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int)
|
||||
case getGenreList(isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getSeriesListByGenre(genreId: Int, isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int)
|
||||
case fetchHome
|
||||
case getRecommendSeriesList
|
||||
case getDayOfWeekSeriesList(dayOfWeek: SeriesPublishedDaysOfWeek, page: Int, size: Int)
|
||||
case getGenreList
|
||||
case getSeriesListByGenre(genreId: Int, page: Int, size: Int)
|
||||
}
|
||||
|
||||
extension SeriesMainApi: TargetType {
|
||||
@@ -39,46 +39,27 @@ extension SeriesMainApi: TargetType {
|
||||
|
||||
var task: Moya.Task {
|
||||
switch self {
|
||||
case .fetchHome(let isAdultContentVisible, let contentType):
|
||||
let parameters = [
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType,
|
||||
] as [String : Any]
|
||||
case .fetchHome:
|
||||
return .requestPlain
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
case .getRecommendSeriesList:
|
||||
return .requestPlain
|
||||
|
||||
case .getRecommendSeriesList(let isAdultContentVisible, let contentType):
|
||||
let parameters = [
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType,
|
||||
] as [String : Any]
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getDayOfWeekSeriesList(let dayOfWeek, let isAdultContentVisible, let contentType, let page, let size):
|
||||
case .getDayOfWeekSeriesList(let dayOfWeek, let page, let size):
|
||||
let parameters = [
|
||||
"dayOfWeek": dayOfWeek,
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType,
|
||||
"page": page - 1,
|
||||
"size": size
|
||||
] as [String : Any]
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getGenreList(let isAdultContentVisible, let contentType):
|
||||
let parameters = [
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType,
|
||||
] as [String : Any]
|
||||
case .getGenreList:
|
||||
return .requestPlain
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getSeriesListByGenre(let genreId, let isAdultContentVisible, let contentType, let page, let size):
|
||||
case .getSeriesListByGenre(let genreId, let page, let size):
|
||||
let parameters = [
|
||||
"genreId": genreId,
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType,
|
||||
"page": page - 1,
|
||||
"size": size
|
||||
] as [String : Any]
|
||||
|
||||
@@ -14,29 +14,17 @@ class SeriesMainRepository {
|
||||
private let api = MoyaProvider<SeriesMainApi>()
|
||||
|
||||
func fetchHome() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.fetchHome(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
return api.requestPublisher(.fetchHome)
|
||||
}
|
||||
|
||||
func getRecommendSeriesList() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getRecommendSeriesList(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
return api.requestPublisher(.getRecommendSeriesList)
|
||||
}
|
||||
|
||||
func getDayOfWeekSeriesList(dayOfWeek: SeriesPublishedDaysOfWeek, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getDayOfWeekSeriesList(
|
||||
dayOfWeek: dayOfWeek,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
|
||||
page: page,
|
||||
size: size
|
||||
)
|
||||
@@ -44,20 +32,13 @@ class SeriesMainRepository {
|
||||
}
|
||||
|
||||
func getGenreList() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getGenreList(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
|
||||
)
|
||||
)
|
||||
return api.requestPublisher(.getGenreList)
|
||||
}
|
||||
|
||||
func getSeriesListByGenre(genreId: Int, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getSeriesListByGenre(
|
||||
genreId: genreId,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
|
||||
page: page,
|
||||
size: size
|
||||
)
|
||||
|
||||
@@ -9,10 +9,10 @@ import Foundation
|
||||
import Moya
|
||||
|
||||
enum SeriesApi {
|
||||
case getSeriesList(creatorId: Int?, isOriginal: Bool, isCompleted: Bool, sortType: SeriesListAllViewModel.SeriesSortType, isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int)
|
||||
case getSeriesDetail(seriesId: Int, isAdultContentVisible: Bool)
|
||||
case getSeriesContentList(seriesId: Int, isAdultContentVisible: Bool, page: Int, size: Int, sortType: SeriesListAllViewModel.SeriesSortType)
|
||||
case getRecommendSeriesList(isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getSeriesList(creatorId: Int?, isOriginal: Bool, isCompleted: Bool, sortType: SeriesListAllViewModel.SeriesSortType, page: Int, size: Int)
|
||||
case getSeriesDetail(seriesId: Int)
|
||||
case getSeriesContentList(seriesId: Int, page: Int, size: Int, sortType: SeriesListAllViewModel.SeriesSortType)
|
||||
case getRecommendSeriesList
|
||||
}
|
||||
|
||||
extension SeriesApi: TargetType {
|
||||
@@ -25,10 +25,10 @@ extension SeriesApi: TargetType {
|
||||
case .getSeriesList:
|
||||
return "/audio-content/series"
|
||||
|
||||
case .getSeriesDetail(let seriesId, _):
|
||||
case .getSeriesDetail(let seriesId):
|
||||
return "/audio-content/series/\(seriesId)"
|
||||
|
||||
case .getSeriesContentList(let seriesId, _, _, _, _):
|
||||
case .getSeriesContentList(let seriesId, _, _, _):
|
||||
return "/audio-content/series/\(seriesId)/content"
|
||||
|
||||
case .getRecommendSeriesList:
|
||||
@@ -45,11 +45,9 @@ extension SeriesApi: TargetType {
|
||||
|
||||
var task: Moya.Task {
|
||||
switch self {
|
||||
case .getSeriesList(let creatorId, let isOriginal, let isCompleted, let sortType, let isAdultContentVisible, let contentType, let page, let size):
|
||||
case .getSeriesList(let creatorId, let isOriginal, let isCompleted, let sortType, let page, let size):
|
||||
var parameters = [
|
||||
"sortType": sortType,
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType,
|
||||
"isOriginal": isOriginal,
|
||||
"isCompleted": isCompleted,
|
||||
"page": page - 1,
|
||||
@@ -62,21 +60,14 @@ extension SeriesApi: TargetType {
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getSeriesDetail(_, let isAdultContentVisible):
|
||||
let parameters = ["isAdultContentVisible": isAdultContentVisible] as [String : Any]
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
case .getSeriesDetail:
|
||||
return .requestPlain
|
||||
|
||||
case .getRecommendSeriesList(let isAdultContentVisible, let contentType):
|
||||
case .getRecommendSeriesList:
|
||||
return .requestPlain
|
||||
|
||||
case .getSeriesContentList(_, let page, let size, let sortType):
|
||||
let parameters = [
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType
|
||||
] as [String : Any]
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getSeriesContentList(_, let isAdultContentVisible, let page, let size, let sortType):
|
||||
let parameters = [
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"page": page - 1,
|
||||
"size": size,
|
||||
"sortType": sortType
|
||||
|
||||
@@ -20,8 +20,6 @@ class SeriesRepository {
|
||||
isOriginal: isOriginal,
|
||||
isCompleted: isCompleted,
|
||||
sortType: sortType,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
|
||||
page: page,
|
||||
size: size
|
||||
)
|
||||
@@ -29,19 +27,13 @@ class SeriesRepository {
|
||||
}
|
||||
|
||||
func getSeriesDetail(seriesId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getSeriesDetail(
|
||||
seriesId: seriesId,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible()
|
||||
)
|
||||
)
|
||||
return api.requestPublisher(.getSeriesDetail(seriesId: seriesId))
|
||||
}
|
||||
|
||||
func getSeriesContentList(seriesId: Int, page: Int, size: Int, sortType: SeriesListAllViewModel.SeriesSortType) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getSeriesContentList(
|
||||
seriesId: seriesId,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
page: page,
|
||||
size: size,
|
||||
sortType: sortType
|
||||
@@ -50,11 +42,6 @@ class SeriesRepository {
|
||||
}
|
||||
|
||||
func getRecommendSeriesList() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getRecommendSeriesList(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
return api.requestPublisher(.getRecommendSeriesList)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,24 +290,6 @@ struct AppStepLayerView: View {
|
||||
case .search:
|
||||
SearchView()
|
||||
|
||||
case .contentMain(let startTab):
|
||||
ContentMainViewV2(selectedTab: startTab)
|
||||
|
||||
case .completedSeriesAll:
|
||||
CompletedSeriesView()
|
||||
|
||||
case .newAlarmContentAll:
|
||||
ContentMainAlarmAllView()
|
||||
|
||||
case .newAsmrContentAll:
|
||||
ContentMainAsmrAllView()
|
||||
|
||||
case .newReplayContentAll:
|
||||
ContentMainReplayAllView()
|
||||
|
||||
case .introduceCreatorAll:
|
||||
ContentMainIntroduceCreatorAllView()
|
||||
|
||||
case .message:
|
||||
MessageView()
|
||||
|
||||
|
||||
@@ -22,6 +22,10 @@ struct ChatTextFieldView: UIViewRepresentable {
|
||||
return true
|
||||
}
|
||||
|
||||
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
|
||||
return parent.isEnabled
|
||||
}
|
||||
|
||||
@objc func textDidChange(_ textField: UITextField) {
|
||||
parent.text = textField.text ?? ""
|
||||
}
|
||||
@@ -29,6 +33,7 @@ struct ChatTextFieldView: UIViewRepresentable {
|
||||
|
||||
@Binding var text: String
|
||||
var placeholder: String
|
||||
var isEnabled: Bool = true
|
||||
var onSend: () -> Void
|
||||
|
||||
func makeUIView(context: Context) -> UITextField {
|
||||
@@ -44,6 +49,7 @@ struct ChatTextFieldView: UIViewRepresentable {
|
||||
textField.tintColor = UIColor(hex: "3BB9F1")
|
||||
textField.font = UIFont(name: Font.preMedium.rawValue, size: 13.3)
|
||||
textField.returnKeyType = .send
|
||||
textField.isEnabled = isEnabled
|
||||
textField.setContentHuggingPriority(.defaultLow, for: .horizontal) // 우선순위 낮추기
|
||||
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) // 우선순위 낮추기
|
||||
textField.addTarget(context.coordinator, action: #selector(Coordinator.textDidChange(_:)), for: .editingChanged)
|
||||
@@ -51,7 +57,13 @@ struct ChatTextFieldView: UIViewRepresentable {
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UITextField, context: Context) {
|
||||
context.coordinator.parent = self
|
||||
uiView.text = text
|
||||
uiView.isEnabled = isEnabled
|
||||
|
||||
if !isEnabled && uiView.isFirstResponder {
|
||||
uiView.resignFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
|
||||
@@ -12,7 +12,7 @@ enum ExplorerApi {
|
||||
case getCreatorRank
|
||||
case getExplorer
|
||||
case searchChannel(channel: String)
|
||||
case getCreatorProfile(userId: Int, isAdultContentVisible: Bool)
|
||||
case getCreatorProfile(userId: Int)
|
||||
case getCreatorDetail(userId: Int)
|
||||
case getFollowerList(userId: Int, page: Int, size: Int)
|
||||
case getCreatorProfileCheers(userId: Int, page: Int, size: Int)
|
||||
@@ -40,7 +40,7 @@ extension ExplorerApi: TargetType {
|
||||
case .searchChannel:
|
||||
return "/explorer/search/channel"
|
||||
|
||||
case .getCreatorProfile(let userId, _):
|
||||
case .getCreatorProfile(let userId):
|
||||
return "/explorer/profile/\(userId)"
|
||||
|
||||
case .getCreatorDetail(let userId):
|
||||
@@ -90,8 +90,8 @@ extension ExplorerApi: TargetType {
|
||||
case .searchChannel(let channel):
|
||||
return .requestParameters(parameters: ["channel" : channel], encoding: URLEncoding.queryString)
|
||||
|
||||
case .getCreatorProfile(_, let isAdultContentVisible):
|
||||
let parameters = ["isAdultContentVisible": isAdultContentVisible, "timezone": TimeZone.current.identifier] as [String: Any]
|
||||
case .getCreatorProfile:
|
||||
let parameters = ["timezone": TimeZone.current.identifier] as [String: Any]
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getFollowerList(_, let page, let size):
|
||||
|
||||
@@ -22,12 +22,7 @@ final class ExplorerRepository {
|
||||
}
|
||||
|
||||
func getCreatorProfile(id: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getCreatorProfile(
|
||||
userId: id,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible()
|
||||
)
|
||||
)
|
||||
return api.requestPublisher(.getCreatorProfile(userId: id))
|
||||
}
|
||||
|
||||
func getCreatorDetail(id: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
|
||||
@@ -25,6 +25,7 @@ enum UserDefaultsKey: String, CaseIterable {
|
||||
case notShowingEventPopupId
|
||||
case isAdultContentVisible
|
||||
case contentPreference
|
||||
case countryCode
|
||||
case isAuditionNotification
|
||||
case searchChannel
|
||||
case marketingPid
|
||||
@@ -73,7 +74,7 @@ extension UserDefaults {
|
||||
|
||||
static func isAdultContentVisible() -> Bool {
|
||||
let key = UserDefaultsKey.isAdultContentVisible.rawValue
|
||||
return UserDefaults.standard.object(forKey: key) != nil ? bool(forKey: .isAdultContentVisible) : true
|
||||
return UserDefaults.standard.object(forKey: key) != nil ? bool(forKey: .isAdultContentVisible) : false
|
||||
}
|
||||
|
||||
static func reset() {
|
||||
|
||||
@@ -9,11 +9,11 @@ import Foundation
|
||||
import Moya
|
||||
|
||||
enum HomeApi {
|
||||
case getHomeData(isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getLatestContentByTheme(theme: String, isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getDayOfWeekSeriesList(dayOfWeek: SeriesPublishedDaysOfWeek, isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getRecommendContents(isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getContentRankingBySort(sort: ContentRankingSortType, isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getHomeData
|
||||
case getLatestContentByTheme(theme: String)
|
||||
case getDayOfWeekSeriesList(dayOfWeek: SeriesPublishedDaysOfWeek)
|
||||
case getRecommendContents
|
||||
case getContentRankingBySort(sort: ContentRankingSortType)
|
||||
}
|
||||
|
||||
extension HomeApi: TargetType {
|
||||
@@ -46,46 +46,33 @@ extension HomeApi: TargetType {
|
||||
|
||||
var task: Moya.Task {
|
||||
switch self {
|
||||
case .getHomeData(let isAdultContentVisible, let contentType):
|
||||
case .getHomeData:
|
||||
let parameters = [
|
||||
"timezone": TimeZone.current.identifier,
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType
|
||||
"timezone": TimeZone.current.identifier
|
||||
] as [String: Any]
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getLatestContentByTheme(let theme, let isAdultContentVisible, let contentType):
|
||||
case .getLatestContentByTheme(let theme):
|
||||
let parameters = [
|
||||
"theme": theme,
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType
|
||||
"theme": theme
|
||||
] as [String: Any]
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getDayOfWeekSeriesList(let dayOfWeek, let isAdultContentVisible, let contentType):
|
||||
case .getDayOfWeekSeriesList(let dayOfWeek):
|
||||
let parameters = [
|
||||
"dayOfWeek": dayOfWeek,
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType
|
||||
"dayOfWeek": dayOfWeek
|
||||
] as [String: Any]
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getRecommendContents(let isAdultContentVisible, let contentType):
|
||||
let parameters = [
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType
|
||||
] as [String: Any]
|
||||
case .getRecommendContents:
|
||||
return .requestPlain
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
case .getContentRankingBySort(let sort, let isAdultContentVisible, let contentType):
|
||||
case .getContentRankingBySort(let sort):
|
||||
let parameters = [
|
||||
"sort": sort,
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType
|
||||
"sort": sort
|
||||
] as [String: Any]
|
||||
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||
|
||||
@@ -14,50 +14,22 @@ class HomeTabRepository {
|
||||
private let api = MoyaProvider<HomeApi>()
|
||||
|
||||
func fetchData() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getHomeData(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
return api.requestPublisher(.getHomeData)
|
||||
}
|
||||
|
||||
func getLatestContentByTheme(theme: String) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getLatestContentByTheme(
|
||||
theme: theme,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
return api.requestPublisher(.getLatestContentByTheme(theme: theme))
|
||||
}
|
||||
|
||||
func getDayOfWeekSeriesList(dayOfWeek: SeriesPublishedDaysOfWeek) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getDayOfWeekSeriesList(
|
||||
dayOfWeek: dayOfWeek,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
return api.requestPublisher(.getDayOfWeekSeriesList(dayOfWeek: dayOfWeek))
|
||||
}
|
||||
|
||||
func getRecommendContents() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getRecommendContents(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
return api.requestPublisher(.getRecommendContents)
|
||||
}
|
||||
|
||||
func getContentRankingBySort(sort: ContentRankingSortType) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getContentRankingBySort(
|
||||
sort: sort,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
return api.requestPublisher(.getContentRankingBySort(sort: sort))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,14 @@ struct HomeTabView: View {
|
||||
AppState.shared.setAppStep(step: .login)
|
||||
return
|
||||
}
|
||||
if auth == false {
|
||||
|
||||
let normalizedCountryCode = UserDefaults
|
||||
.string(forKey: .countryCode)
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.uppercased()
|
||||
let isKoreanCountry = normalizedCountryCode.isEmpty || normalizedCountryCode == "KR"
|
||||
|
||||
if isKoreanCountry && auth == false {
|
||||
pendingAction = {
|
||||
AppState.shared
|
||||
.setAppStep(step: .characterDetail(characterId: characterId))
|
||||
@@ -52,6 +59,15 @@ struct HomeTabView: View {
|
||||
isShowAuthConfirmView = true
|
||||
return
|
||||
}
|
||||
|
||||
if !UserDefaults.isAdultContentVisible() {
|
||||
pendingAction = nil
|
||||
AppState.shared.errorMessage = I18n.Settings.adultContentEnableGuide
|
||||
AppState.shared.isShowErrorPopup = true
|
||||
AppState.shared.setAppStep(step: .contentViewSettings)
|
||||
return
|
||||
}
|
||||
|
||||
AppState.shared.setAppStep(step: .characterDetail(characterId: characterId))
|
||||
}
|
||||
|
||||
|
||||
@@ -408,6 +408,30 @@ enum I18n {
|
||||
// 알림 다이얼로그 타이틀
|
||||
static var alertTitle: String { pick(ko: "알림", en: "Notice", ja: "お知らせ") }
|
||||
|
||||
static var adultContentAgeCheckTitle: String {
|
||||
pick(
|
||||
ko: "당신은 18세 이상입니까?",
|
||||
en: "Are you over 18 years old?",
|
||||
ja: "あなたは18歳以上ですか?"
|
||||
)
|
||||
}
|
||||
|
||||
static var adultContentAgeCheckDesc: String {
|
||||
pick(
|
||||
ko: "해당 콘텐츠는 18세 이상만 이용이 가능합니다!",
|
||||
en: "This content is available only to users aged 18 and over!",
|
||||
ja: "このコンテンツは18歳以上のみ利用可能です!"
|
||||
)
|
||||
}
|
||||
|
||||
static var adultContentEnableGuide: String {
|
||||
pick(
|
||||
ko: "민감한 콘텐츠를 보려면 콘텐츠 보기 설정에서 민감한 콘텐츠 보기를 켜주세요.",
|
||||
en: "To view sensitive content, turn on Sensitive Content in Content View Settings.",
|
||||
ja: "センシティブなコンテンツを表示するには、コンテンツ表示設定でセンシティブなコンテンツ表示をオンにしてください。"
|
||||
)
|
||||
}
|
||||
|
||||
// 로그아웃 확인 질문
|
||||
static var logoutQuestion: String {
|
||||
pick(
|
||||
@@ -773,6 +797,8 @@ enum I18n {
|
||||
|
||||
static var signatureOn: String { pick(ko: "시그 ON", en: "Sign ON", ja: "シグ ON") }
|
||||
static var signatureOff: String { pick(ko: "시그 OFF", en: "Sign OFF", ja: "シグ OFF") }
|
||||
static var chatFreezeOn: String { pick(ko: "얼림 ON", en: "Freeze ON", ja: "凍結 ON") }
|
||||
static var chatFreezeOff: String { pick(ko: "얼림 OFF", en: "Freeze OFF", ja: "凍結 OFF") }
|
||||
static var captionOn: String { pick(ko: "자막 ON", en: "Caption ON", ja: "字幕 ON") }
|
||||
static var captionOff: String { pick(ko: "자막 OFF", en: "Caption OFF", ja: "字幕 OFF") }
|
||||
static var backgroundOn: String { pick(ko: "배경 ON", en: "Back ON", ja: "背景 ON") }
|
||||
@@ -782,6 +808,11 @@ enum I18n {
|
||||
static var participants: String { pick(ko: "참여자", en: "Participants", ja: "リスナー") }
|
||||
static var follow: String { pick(ko: "팔로우", en: "Follow", ja: "フォロー") }
|
||||
static var following: String { pick(ko: "팔로잉", en: "Following", ja: "フォロー中") }
|
||||
static var chatFreezeOnStatusMessageForCreator: String { pick(ko: "“🧊 모두들 얼음!” 채팅창을 얼렸습니다.", en: "\"🧊 Freeze, everyone!\" The chat has been frozen.", ja: "「🧊 みんなフリーズ!」チャットを凍結しました。") }
|
||||
static var chatFreezeOnStatusMessageForListener: String { pick(ko: "“🧊 모두들 얼음!” 채팅창이 얼었습니다.", en: "\"🧊 Freeze, everyone!\" The chat is now frozen.", ja: "「🧊 みんなフリーズ!」チャットが凍結されました。") }
|
||||
static var chatFreezeOffStatusMessage: String { pick(ko: "“💧땡! “ 채팅창 얼리기가 해제되었습니다.", en: "\"💧 Ding!\" Chat freeze has been lifted.", ja: "「💧 たん!」チャット凍結が解除されました。") }
|
||||
static var chatFreezeBlockedMessage: String { pick(ko: "🧊 채팅창이 얼었습니다.", en: "🧊 The chat is now frozen.", ja: "🧊 チャットが凍結されました。") }
|
||||
static var chatDeleteTitle: String { pick(ko: "채팅 삭제", en: "Delete chat", ja: "チャット削除") }
|
||||
}
|
||||
|
||||
enum LiveNow {
|
||||
|
||||
@@ -30,6 +30,7 @@ enum LiveApi {
|
||||
case setListener(request: SetManagerOrSpeakerOrAudienceRequest)
|
||||
case setSpeaker(request: SetManagerOrSpeakerOrAudienceRequest)
|
||||
case setManager(request: SetManagerOrSpeakerOrAudienceRequest)
|
||||
case setChatFreeze(request: SetChatFreezeRequest)
|
||||
case kickOut(request: LiveRoomKickOutRequest)
|
||||
case donationStatus(roomId: Int)
|
||||
case donationTotal(roomId: Int)
|
||||
@@ -40,7 +41,7 @@ enum LiveApi {
|
||||
case likeHeart(request: LiveRoomLikeHeartRequest)
|
||||
case getTotalHeartCount(roomId: Int)
|
||||
case heartStatus(roomId: Int)
|
||||
case getLiveMain(isAdultContentVisible: Bool, contentType: ContentType)
|
||||
case getLiveMain
|
||||
}
|
||||
|
||||
extension LiveApi: TargetType {
|
||||
@@ -112,6 +113,9 @@ extension LiveApi: TargetType {
|
||||
|
||||
case .setManager:
|
||||
return "/live/room/info/set/manager"
|
||||
|
||||
case .setChatFreeze:
|
||||
return "/live/room/info/set/chat-freeze"
|
||||
|
||||
case .kickOut:
|
||||
return "/live/room/kick-out"
|
||||
@@ -156,7 +160,7 @@ extension LiveApi: TargetType {
|
||||
case .makeReservation, .enterRoom, .createRoom, .quitRoom, .donation, .refundDonation, .kickOut, .likeHeart:
|
||||
return .post
|
||||
|
||||
case .setListener, .setSpeaker, .setManager, .cancelReservation, .startLive, .cancelRoom, .editLiveRoomInfo:
|
||||
case .setListener, .setSpeaker, .setManager, .setChatFreeze, .cancelReservation, .startLive, .cancelRoom, .editLiveRoomInfo:
|
||||
return .put
|
||||
|
||||
case .deleteDonationMessage:
|
||||
@@ -237,6 +241,9 @@ extension LiveApi: TargetType {
|
||||
|
||||
case .setListener(let request), .setSpeaker(let request), .setManager(let request):
|
||||
return .requestJSONEncodable(request)
|
||||
|
||||
case .setChatFreeze(let request):
|
||||
return .requestJSONEncodable(request)
|
||||
|
||||
case .kickOut(let request):
|
||||
return .requestJSONEncodable(request)
|
||||
@@ -253,11 +260,9 @@ extension LiveApi: TargetType {
|
||||
case .likeHeart(let request):
|
||||
return .requestJSONEncodable(request)
|
||||
|
||||
case .getLiveMain(let isAdultContentVisible, let contentType):
|
||||
case .getLiveMain:
|
||||
let parameters = [
|
||||
"timezone": TimeZone.current.identifier,
|
||||
"isAdultContentVisible": isAdultContentVisible,
|
||||
"contentType": contentType
|
||||
"timezone": TimeZone.current.identifier
|
||||
] as [String: Any]
|
||||
|
||||
return .requestParameters(
|
||||
|
||||
@@ -92,6 +92,10 @@ final class LiveRepository {
|
||||
func setManager(roomId: Int, userId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.setManager(request: SetManagerOrSpeakerOrAudienceRequest(roomId: roomId, memberId: userId)))
|
||||
}
|
||||
|
||||
func setChatFreeze(roomId: Int, isChatFrozen: Bool) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.setChatFreeze(request: SetChatFreezeRequest(roomId: roomId, isChatFrozen: isChatFrozen)))
|
||||
}
|
||||
|
||||
func kickOut(roomId: Int, userId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.kickOut(request: LiveRoomKickOutRequest(roomId: roomId, userId: userId)))
|
||||
@@ -141,11 +145,6 @@ final class LiveRepository {
|
||||
}
|
||||
|
||||
func getLiveMain() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getLiveMain(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
return api.requestPublisher(.getLiveMain)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user