17 Commits

Author SHA1 Message Date
Yu Sung
d369bc11f7 fix(api): 콘텐츠 설정 PATCH 제외 API 파라미터를 제거한다 2026-03-27 22:28:21 +09:00
Yu Sung
f542191d46 fix(age-setting): 국가별 연령제한 노출 조건을 통일한다 2026-03-27 18:22:53 +09:00
Yu Sung
1d120b58bd fix(content): 성인 콘텐츠 설정 동기화와 국가별 인증 분기를 적용한다 2026-03-27 17:34:02 +09:00
Yu Sung
44daabdcae fix(live-room): 캡쳐 보호 음소거 동기화 2026-03-24 19:19:29 +09:00
Yu Sung
0844c6f4d7 fix(live-room): 라이브 상세 SNS 링크 아이콘 매핑을 신규 필드에 맞춘다 2026-03-24 13:36:10 +09:00
Yu Sung
c6a6b3c79e fix(live-room): 라이브 상세 복귀 시 DIM만 보이는 상태를 방지한다 2026-03-24 12:05:25 +09:00
Yu Sung
4f66ffb595 docs(live-room): 채팅 얼림 아이콘 이동 점검 문서를 추가한다 2026-03-20 15:59:09 +09:00
Yu Sung
91b5ed974f fix(live-room): 채팅 얼림 버튼 위치와 차단 문구를 정렬한다 2026-03-20 15:58:31 +09:00
Yu Sung
af31444f0f fix(live-room): 채팅창 얼림 버튼 위치와 안내 문구를 조정한다 2026-03-20 14:27:10 +09:00
Yu Sung
8eca5df62b feat(live-room): 라이브룸 채팅 삭제 기능 구현 2026-03-20 10:51:22 +09:00
Yu Sung
793b5dd95a fix(live-room): 채팅 금지 입력 차단 안내를 즉시 노출한다 2026-03-19 18:46:41 +09:00
Yu Sung
70003af82b feat(live-room): 채팅창 얼리기 기능을 추가한다
채팅 입력 제어와 룸 상태 동기화를 통합해 지연 입장자도 동일 상태를 적용한다.
2026-03-19 18:20:13 +09:00
Yu Sung
0a22f87acc refactor: 사용하지 않는 파일 삭제 2026-03-18 19:45:45 +09:00
Yu Sung
8b102905ad refactor: 사용하지 않는 파일 삭제 2026-03-18 19:14:54 +09:00
Yu Sung
e9fc7e180d fix(live): 라이브룸 후원·하트 랭킹 왕관 UI를 동일화한다 2026-03-18 14:26:00 +09:00
Yu Sung
84d8e2f2e3 fix(chat): 채팅 왕관 오버레이 정렬 2026-03-18 14:06:43 +09:00
Yu Sung
b51d643db8 feat(home): 홈 탭 지연 로딩 및 상태 유지 구현 2026-03-18 11:30:04 +09:00
152 changed files with 3288 additions and 7561 deletions

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -1276,6 +1276,7 @@
} }
}, },
"※ 인기 순위는 매주 업데이트됩니다." : { "※ 인기 순위는 매주 업데이트됩니다." : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -1324,6 +1325,7 @@
} }
}, },
"※ 최근 2주간 등록된 새로운 ASMR 입니다." : { "※ 최근 2주간 등록된 새로운 ASMR 입니다." : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -1340,6 +1342,7 @@
} }
}, },
"※ 최근 2주간 등록된 새로운 라이브 다시듣기 입니다." : { "※ 최근 2주간 등록된 새로운 라이브 다시듣기 입니다." : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -1356,6 +1359,7 @@
} }
}, },
"※ 최근 2주간 등록된 새로운 알람 입니다." : { "※ 최근 2주간 등록된 새로운 알람 입니다." : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -3938,6 +3942,7 @@
} }
}, },
"마이페이지에서 본인인증을 해주세요" : { "마이페이지에서 본인인증을 해주세요" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -4129,22 +4134,6 @@
} }
} }
}, },
"모집완료" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Recruitment closed"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "募集終了"
}
}
}
},
"목" : { "목" : {
"localizations" : { "localizations" : {
"en" : { "en" : {
@@ -4179,6 +4168,22 @@
}, },
"모서리 원을 드래그해서 크롭 영역 크기를 조정하세요" : { "모서리 원을 드래그해서 크롭 영역 크기를 조정하세요" : {
},
"모집완료" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Recruitment closed"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "募集終了"
}
}
}
}, },
"모집중" : { "모집중" : {
"localizations" : { "localizations" : {
@@ -4597,6 +4602,7 @@
} }
}, },
"보이스 모닝콜" : { "보이스 모닝콜" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -5173,6 +5179,7 @@
} }
}, },
"새로운 콘텐츠" : { "새로운 콘텐츠" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -5384,6 +5391,7 @@
} }
}, },
"숏플" : { "숏플" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -6459,6 +6467,7 @@
} }
}, },
"오리지널 오디오 드라마" : { "오리지널 오디오 드라마" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -7051,6 +7060,7 @@
} }
}, },
"인기 시리즈" : { "인기 시리즈" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -7179,6 +7189,7 @@
} }
}, },
"일간 랭킹" : { "일간 랭킹" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -7339,6 +7350,7 @@
} }
}, },
"자세히 >" : { "자세히 >" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -7515,6 +7527,7 @@
} }
}, },
"장르별 추천 시리즈" : { "장르별 추천 시리즈" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -8801,6 +8814,7 @@
} }
}, },
"콘텐츠 마켓" : { "콘텐츠 마켓" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -9121,6 +9135,7 @@
} }
}, },
"태그별 추천 콘텐츠" : { "태그별 추천 콘텐츠" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {

View File

@@ -74,6 +74,7 @@ class AppState: ObservableObject {
@Published var isShowErrorPopup = false @Published var isShowErrorPopup = false
@Published var errorMessage = "" @Published var errorMessage = ""
@Published private var pendingContentSettingsGuideMessage: String? = nil
@Published var liveDetailSheet: LiveDetailSheetState? = nil @Published var liveDetailSheet: LiveDetailSheetState? = nil
private func syncStepWithNavigationPath() { private func syncStepWithNavigationPath() {
@@ -181,6 +182,16 @@ class AppState: ObservableObject {
pendingCommunityCommentPostId = 0 pendingCommunityCommentPostId = 0
} }
func setPendingContentSettingsGuideMessage(_ message: String) {
pendingContentSettingsGuideMessage = message
}
func consumePendingContentSettingsGuideMessage() -> String? {
let message = pendingContentSettingsGuideMessage
pendingContentSettingsGuideMessage = nil
return message
}
// ( -> ) UI // ( -> ) UI
func softRestart() { func softRestart() {
isRestartApp = true isRestartApp = true

View File

@@ -150,18 +150,6 @@ enum AppStep {
case search case search
case contentMain(startTab: ContentMainTab)
case completedSeriesAll
case newAlarmContentAll
case newAsmrContentAll
case newReplayContentAll
case introduceCreatorAll
case message case message
case notificationList case notificationList

View File

@@ -75,6 +75,9 @@ final class AppViewModel: ObservableObject {
UserDefaults.set(data.isAuth, forKey: .auth) UserDefaults.set(data.isAuth, forKey: .auth)
UserDefaults.set(data.role.rawValue, forKey: .role) UserDefaults.set(data.role.rawValue, forKey: .role)
UserDefaults.set(data.auditionNotice ?? false, forKey: .isAuditionNotification) 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 { if data.followingChannelLiveNotice == nil && data.followingChannelUploadContentNotice == nil && data.messageNotice == nil {
AppState.shared.isShowNotificationSettingsDialog = true AppState.shared.isShowNotificationSettingsDialog = true
} }

View File

@@ -41,7 +41,14 @@ struct ChatTabView: View {
AppState.shared.setAppStep(step: .login) AppState.shared.setAppStep(step: .login)
return 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 = { pendingAction = {
AppState.shared AppState.shared
.setAppStep(step: .characterDetail(characterId: characterId)) .setAppStep(step: .characterDetail(characterId: characterId))
@@ -49,9 +56,21 @@ struct ChatTabView: View {
isShowAuthConfirmView = true isShowAuthConfirmView = true
return return
} }
if !UserDefaults.isAdultContentVisible() {
pendingAction = nil
moveToContentSettingsWithGuideToast()
return
}
AppState.shared.setAppStep(step: .characterDetail(characterId: characterId)) AppState.shared.setAppStep(step: .characterDetail(characterId: characterId))
} }
private func moveToContentSettingsWithGuideToast() {
AppState.shared.setPendingContentSettingsGuideMessage(I18n.Settings.adultContentEnableGuide)
AppState.shared.setAppStep(step: .contentViewSettings)
}
private func handleCharacterSelection() { private func handleCharacterSelection() {
let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines) let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { guard !trimmed.isEmpty else {

View File

@@ -9,7 +9,7 @@ import Foundation
import Moya import Moya
enum ContentApi { 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 getAudioContentDetail(audioContentId: Int)
case likeContent(request: PutAudioContentLikeRequest) case likeContent(request: PutAudioContentLikeRequest)
case registerComment(request: RegisterAudioContentCommentRequest) case registerComment(request: RegisterAudioContentCommentRequest)
@@ -25,51 +25,51 @@ enum ContentApi {
case getNewContentUploadCreatorList case getNewContentUploadCreatorList
case getMainBannerList case getMainBannerList
case getMainOrderList case getMainOrderList
case getNewContentOfTheme(theme: String, isAdultContentVisible: Bool, contentType: ContentType) case getNewContentOfTheme(theme: String)
case getCurationList(isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int) case getCurationList(page: Int, size: Int)
case donation(request: AudioContentDonationRequest) case donation(request: AudioContentDonationRequest)
case modifyComment(request: ModifyCommentRequest) case modifyComment(request: ModifyCommentRequest)
case getNewContentThemeList(isAdultContentVisible: Bool, contentType: ContentType) case getNewContentThemeList
case getNewContentAllOfTheme(isFree: Bool, theme: String, isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int) case getNewContentAllOfTheme(isFree: Bool, theme: String, page: Int, size: Int)
case getAudioContentListByCurationId(curationId: Int, isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int, sort: ContentCurationViewModel.Sort) case getAudioContentListByCurationId(curationId: Int, page: Int, size: Int, sort: ContentCurationViewModel.Sort)
case getContentRanking(isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int, sortType: String) case getContentRanking(page: Int, size: Int, sortType: String)
case getContentRankingSortType case getContentRankingSortType
case pinContent(contentId: Int) case pinContent(contentId: Int)
case unpinContent(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 generateUrl(contentId: Int)
case getContentMainHome(isAdultContentVisible: Bool, contentType: ContentType) case getContentMainHome
case getPopularContentByCreator(creatorId: Int, isAdultContentVisible: Bool, contentType: ContentType) case getPopularContentByCreator(creatorId: Int)
case getContentMainHomeContentRanking(isAdultContentVisible: Bool, contentType: ContentType, sortType: String) case getContentMainHomeContentRanking(sortType: String)
case getContentMainSeries(isAdultContentVisible: Bool, contentType: ContentType) case getContentMainSeries
case getRecommendSeriesListByGenre(genreId: Int, isAdultContentVisible: Bool, contentType: ContentType) case getRecommendSeriesListByGenre(genreId: Int)
case getRecommendSeriesByCreator(creatorId: Int, isAdultContentVisible: Bool, contentType: ContentType) case getRecommendSeriesByCreator(creatorId: Int)
case getCompletedSeries(isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int) case getCompletedSeries(page: Int, size: Int)
case getContentMainContent(isAdultContentVisible: Bool, contentType: ContentType) case getContentMainContent
case getContentMainNewContentOfTheme(theme: String, isAdultContentVisible: Bool, contentType: ContentType) case getContentMainNewContentOfTheme(theme: String)
case getDailyContentRanking(sortType: String, isAdultContentVisible: Bool, contentType: ContentType) case getDailyContentRanking(sortType: String)
case getRecommendContentByTag(tag: String, contentType: ContentType) case getRecommendContentByTag(tag: String)
case getContentMainContentPopularContentByCreator(creatorId: Int, isAdultContentVisible: Bool, contentType: ContentType) case getContentMainContentPopularContentByCreator(creatorId: Int)
case getContentMainAlarm(isAdultContentVisible: Bool, contentType: ContentType) case getContentMainAlarm
case getContentMainAlarmAll(theme: String, isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int) case getContentMainAlarmAll(theme: String, page: Int, size: Int)
case getContentMainAsmr(isAdultContentVisible: Bool, contentType: ContentType) case getContentMainAsmr
case getPopularAsmrContentByCreator(creatorId: Int, isAdultContentVisible: Bool, contentType: ContentType) case getPopularAsmrContentByCreator(creatorId: Int)
case getContentMainReplay(isAdultContentVisible: Bool, contentType: ContentType) case getContentMainReplay
case getPopularReplayContentByCreator(creatorId: Int, isAdultContentVisible: Bool, contentType: ContentType) case getPopularReplayContentByCreator(creatorId: Int)
case getContentMainFree(isAdultContentVisible: Bool, contentType: ContentType) case getContentMainFree
case getIntroduceCreatorList(isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int) case getIntroduceCreatorList(page: Int, size: Int)
case getNewFreeContentOfTheme(theme: String, isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int) case getNewFreeContentOfTheme(theme: String, page: Int, size: Int)
case getPopularFreeContentByCreator(creatorId: Int, isAdultContentVisible: Bool, contentType: ContentType) 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 getAllAudioContents(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 getAudioContentActiveThemeList(isFree: Bool?, isPointAvailableOnly: Bool?)
} }
extension ContentApi: TargetType { extension ContentApi: TargetType {
@@ -145,7 +145,7 @@ extension ContentApi: TargetType {
case .getNewContentAllOfTheme: case .getNewContentAllOfTheme:
return "/audio-content/main/new/all" return "/audio-content/main/new/all"
case .getAudioContentListByCurationId(let curationId, _, _, _, _, _): case .getAudioContentListByCurationId(let curationId, _, _, _):
return "/audio-content/curation/\(curationId)" return "/audio-content/curation/\(curationId)"
case .getContentRanking: case .getContentRanking:
@@ -160,7 +160,7 @@ extension ContentApi: TargetType {
case .unpinContent(let contentId): case .unpinContent(let contentId):
return "/audio-content/unpin-at-the-top/\(contentId)" return "/audio-content/unpin-at-the-top/\(contentId)"
case .getAudioContentByTheme(let themeId, _, _, _, _, _): case .getAudioContentByTheme(let themeId, _, _, _):
return "/audio-content/theme/\(themeId)/content" return "/audio-content/theme/\(themeId)/content"
case .generateUrl(let contentId): case .generateUrl(let contentId):
@@ -273,11 +273,10 @@ extension ContentApi: TargetType {
var task: Moya.Task { var task: Moya.Task {
switch self { 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 = [ let parameters = [
"creator-id": userId, "creator-id": userId,
"category-id": categoryId, "category-id": categoryId,
"isAdultContentVisible": isAdultContentVisible,
"page": page - 1, "page": page - 1,
"size": size, "size": size,
"sort-type": sort "sort-type": sort
@@ -339,11 +338,9 @@ extension ContentApi: TargetType {
case .deleteAudioContent: case .deleteAudioContent:
return .requestPlain return .requestPlain
case .getNewContentOfTheme(let theme, let isAdultContentVisible, let contentType): case .getNewContentOfTheme(let theme):
let parameters = [ let parameters = [
"theme": theme, "theme": theme
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType
] as [String : Any] ] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
@@ -354,30 +351,21 @@ extension ContentApi: TargetType {
case .modifyComment(let request): case .modifyComment(let request):
return .requestJSONEncodable(request) return .requestJSONEncodable(request)
case .getNewContentThemeList(let isAdultContentVisible, let contentType): case .getNewContentThemeList:
let parameters = [ return .requestPlain
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType
] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) case .getNewContentAllOfTheme(let isFree, let theme, let page, let size):
case .getNewContentAllOfTheme(let isFree, let theme, let isAdultContentVisible, let contentType, let page, let size):
let parameters = [ let parameters = [
"isFree": isFree, "isFree": isFree,
"theme": theme, "theme": theme,
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType,
"page": page - 1, "page": page - 1,
"size": size "size": size
] as [String : Any] ] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) 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 = [ let parameters = [
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType,
"page": page - 1, "page": page - 1,
"size": size, "size": size,
"sort-type": sort "sort-type": sort
@@ -385,10 +373,8 @@ extension ContentApi: TargetType {
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) 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 = [ let parameters = [
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType,
"page": page - 1, "page": page - 1,
"size": size, "size": size,
"sort-type": sortType "sort-type": sortType
@@ -399,10 +385,8 @@ extension ContentApi: TargetType {
case .getContentRankingSortType: case .getContentRankingSortType:
return .requestPlain return .requestPlain
case .getCurationList(let isAdultContentVisible, let contentType, let page, let size): case .getCurationList(let page, let size):
let parameters = [ let parameters = [
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType,
"page": page - 1, "page": page - 1,
"size": size "size": size
] as [String : Any] ] as [String : Any]
@@ -412,10 +396,8 @@ extension ContentApi: TargetType {
case .pinContent, .unpinContent: case .pinContent, .unpinContent:
return .requestPlain return .requestPlain
case .getAudioContentByTheme(_, let isAdultContentVisible, let contentType, let page, let size, let sort): case .getAudioContentByTheme(_, let page, let size, let sort):
let parameters = [ let parameters = [
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType,
"page": page - 1, "page": page - 1,
"size": size, "size": size,
"sort-type": sort "sort-type": sort
@@ -423,141 +405,109 @@ extension ContentApi: TargetType {
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .getContentMainHome(let isAdultContentVisible, let contentType), case .getContentMainHome,
.getContentMainSeries(let isAdultContentVisible, let contentType), .getContentMainSeries,
.getContentMainContent(let isAdultContentVisible, let contentType), .getContentMainContent,
.getContentMainAlarm(let isAdultContentVisible, let contentType), .getContentMainAlarm,
.getContentMainAsmr(let isAdultContentVisible, let contentType), .getContentMainAsmr,
.getContentMainReplay(let isAdultContentVisible, let contentType), .getContentMainReplay,
.getContentMainFree(let isAdultContentVisible, let contentType): .getContentMainFree:
return .requestPlain
case .getRecommendSeriesListByGenre(let genreId):
let parameters = [ let parameters = [
"isAdultContentVisible": isAdultContentVisible, "genreId": genreId
"contentType": contentType
] as [String : Any] ] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .getRecommendSeriesListByGenre(let genreId, let isAdultContentVisible, let contentType): case .getPopularContentByCreator(let creatorId):
let parameters = [ let parameters = [
"genreId": genreId, "creatorId": creatorId
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType
] as [String : Any] ] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .getPopularContentByCreator(let creatorId, let isAdultContentVisible, let contentType): case .getContentMainHomeContentRanking(let sortType):
let parameters = [ let parameters = [
"creatorId": creatorId, "sort-type": sortType
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType
] as [String : Any] ] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .getContentMainHomeContentRanking(let isAdultContentVisible, let contentType, let sortType): case .getRecommendSeriesByCreator(let creatorId):
let parameters = [ let parameters = [
"sort-type": sortType, "creatorId": creatorId
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType
] as [String : Any] ] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .getRecommendSeriesByCreator(let creatorId, let isAdultContentVisible, let contentType): case .getContentMainNewContentOfTheme(let theme):
let parameters = [ let parameters = [
"creatorId": creatorId, "theme": theme
"isAdultContentVisible": isAdultContentVisible, ] as [String : Any]
"contentType": contentType 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] ] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) 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 = [ let parameters = [
"theme": theme, "theme": theme,
"isAdultContentVisible": isAdultContentVisible, "page": page - 1,
"contentType": contentType "size": size
] as [String : Any] ] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .getDailyContentRanking(let sortType, let isAdultContentVisible, let contentType): case .getContentMainAlarmAll(let theme, let page, let size):
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):
let parameters = [ let parameters = [
"theme": theme, "theme": theme,
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType,
"page": page - 1, "page": page - 1,
"size": size "size": size
] as [String : Any] ] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) 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 = [ let parameters = [
"theme": theme,
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType,
"page": page - 1, "page": page - 1,
"size": size "size": size
] as [String : Any] ] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) 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 = [ let parameters = [
"isAdultContentVisible": isAdultContentVisible, "creatorId": creatorId
"contentType": contentType,
"page": page - 1,
"size": size
] as [String : Any] ] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .getPopularAsmrContentByCreator(let creatorId, let isAdultContentVisible, let contentType), case .getCompletedSeries(let page, let size):
.getPopularReplayContentByCreator(let creatorId, let isAdultContentVisible, let contentType),
.getPopularFreeContentByCreator(let creatorId, let isAdultContentVisible, let contentType):
let parameters = [ 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, "page": page - 1,
"size": size "size": size
] as [String : Any] ] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) 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 = [ var parameters = [
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType,
"sort-type": sortType, "sort-type": sortType,
"page": page - 1, "page": page - 1,
"size": size "size": size
@@ -577,11 +527,8 @@ extension ContentApi: TargetType {
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .getAudioContentActiveThemeList(let isAdultContentVisible, let contentType, let isFree, let isPointAvailableOnly): case .getAudioContentActiveThemeList(let isFree, let isPointAvailableOnly):
var parameters = [ var parameters = [String : Any]()
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType,
] as [String : Any]
if let isFree = isFree { if let isFree = isFree {
parameters["isFree"] = isFree parameters["isFree"] = isFree

View File

@@ -20,7 +20,6 @@ final class ContentRepository {
.getAudioContentList( .getAudioContentList(
userId: userId, userId: userId,
categoryId: categoryId, categoryId: categoryId,
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
page: page, page: page,
size: size, size: size,
sort: sort) sort: sort)
@@ -89,19 +88,13 @@ final class ContentRepository {
func getNewContentOfTheme(theme: String) -> AnyPublisher<Response, MoyaError> { func getNewContentOfTheme(theme: String) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher( return api.requestPublisher(
.getNewContentOfTheme( .getNewContentOfTheme(theme: theme)
theme: theme,
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
)
) )
} }
func getCurationList(page: Int, size: Int) -> AnyPublisher<Response, MoyaError> { func getCurationList(page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher( return api.requestPublisher(
.getCurationList( .getCurationList(
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
page: page, page: page,
size: size size: size
) )
@@ -117,12 +110,7 @@ final class ContentRepository {
} }
func getNewContentThemeList() -> AnyPublisher<Response, MoyaError> { func getNewContentThemeList() -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher( return api.requestPublisher(.getNewContentThemeList)
.getNewContentThemeList(
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
)
)
} }
func getNewContentAllOfTheme(isFree: Bool, theme: String, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> { func getNewContentAllOfTheme(isFree: Bool, theme: String, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
@@ -130,8 +118,6 @@ final class ContentRepository {
.getNewContentAllOfTheme( .getNewContentAllOfTheme(
isFree: isFree, isFree: isFree,
theme: theme, theme: theme,
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
page: page, page: page,
size: size size: size
) )
@@ -142,8 +128,6 @@ final class ContentRepository {
return api.requestPublisher( return api.requestPublisher(
.getAudioContentListByCurationId( .getAudioContentListByCurationId(
curationId: curationId, curationId: curationId,
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
page: page, page: page,
size: size, size: size,
sort: sort sort: sort
@@ -158,8 +142,6 @@ final class ContentRepository {
func getContentRanking(page: Int, size: Int, sortType: String = "매출") -> AnyPublisher<Response, MoyaError> { func getContentRanking(page: Int, size: Int, sortType: String = "매출") -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher( return api.requestPublisher(
.getContentRanking( .getContentRanking(
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
page: page, page: page,
size: size, size: size,
sortType: sortType sortType: sortType
@@ -183,8 +165,6 @@ final class ContentRepository {
return api.requestPublisher( return api.requestPublisher(
.getAudioContentByTheme( .getAudioContentByTheme(
themeId: themeId, themeId: themeId,
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
page: page, page: page,
size: size, size: size,
sort: sort sort: sort
@@ -206,8 +186,6 @@ final class ContentRepository {
) -> AnyPublisher<Response, MoyaError> { ) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher( return api.requestPublisher(
.getAllAudioContents( .getAllAudioContents(
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
page: page, page: page,
size: size, size: size,
isFree: isFree, isFree: isFree,
@@ -221,8 +199,6 @@ final class ContentRepository {
func getAudioContentActiveThemeList(isFree: Bool, isPointAvailableOnly: Bool) -> AnyPublisher<Response, MoyaError> { func getAudioContentActiveThemeList(isFree: Bool, isPointAvailableOnly: Bool) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher( return api.requestPublisher(
.getAudioContentActiveThemeList( .getAudioContentActiveThemeList(
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
isFree: isFree, isFree: isFree,
isPointAvailableOnly: isPointAvailableOnly isPointAvailableOnly: isPointAvailableOnly
) )

View File

@@ -24,6 +24,14 @@ struct ContentCreateView: View {
@State private var isShowSelectTimeView = false @State private var isShowSelectTimeView = false
var body: some View { 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) { BaseView(isLoading: $viewModel.isLoading) {
GeometryReader { proxy in GeometryReader { proxy in
ZStack { ZStack {
@@ -448,34 +456,36 @@ struct ContentCreateView: View {
.padding(.top, 26.7) .padding(.top, 26.7)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
VStack(spacing: 13.3) { if shouldShowAdultSetting {
Text("연령 제한") VStack(spacing: 13.3) {
.appFont(size: 16.7, weight: .bold) Text("연령 제한")
.foregroundColor(Color.grayee) .appFont(size: 16.7, weight: .bold)
.frame(maxWidth: .infinity, alignment: .leading) .foregroundColor(Color.grayee)
.frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 13.3) { HStack(spacing: 13.3) {
SelectButtonView(title: I18n.CreateContent.allAges, isChecked: !viewModel.isAdult) { SelectButtonView(title: I18n.CreateContent.allAges, isChecked: !viewModel.isAdult) {
if viewModel.isAdult { if viewModel.isAdult {
viewModel.isAdult = false viewModel.isAdult = false
}
}
SelectButtonView(title: I18n.CreateContent.over19, isChecked: viewModel.isAdult) {
if !viewModel.isAdult {
viewModel.isAdult = true
}
} }
} }
SelectButtonView(title: I18n.CreateContent.over19, isChecked: viewModel.isAdult) { Text("성인콘텐츠를 전체관람가로 등록할 시 발생하는 법적 책임은 회사와 상관없이 콘텐츠를 등록한 본인에게 있습니다.\n콘텐츠 내용은 물론 제목도 19금 여부를 체크해 주시기 바랍니다.")
if !viewModel.isAdult { .appFont(size: 13.3, weight: .medium)
viewModel.isAdult = true .foregroundColor(Color.mainRed3)
} .frame(maxWidth: .infinity, alignment: .leading)
} .padding(.top, 13.3)
} }
.padding(.top, 26.7)
Text("성인콘텐츠를 전체관람가로 등록할 시 발생하는 법적 책임은 회사와 상관없이 콘텐츠를 등록한 본인에게 있습니다.\n콘텐츠 내용은 물론 제목도 19금 여부를 체크해 주시기 바랍니다.") .padding(.horizontal, 13.3)
.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)
VStack(spacing: 13.3) { VStack(spacing: 13.3) {
Text("댓글 가능 여부") Text("댓글 가능 여부")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,17 +7,6 @@
import Foundation 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 { struct GetAudioContentRanking: Decodable {
let startDate: String let startDate: String
let endDate: String let endDate: String
@@ -37,12 +26,6 @@ struct GetAudioContentRankingItem: Decodable {
let creatorProfileImageUrl: String let creatorProfileImageUrl: String
} }
struct ContentCreatorResponse: Decodable {
let creatorId: Int
let creatorNickname: String
let creatorProfileImageUrl: String
}
struct GetAudioContentMainItem: Decodable { struct GetAudioContentMainItem: Decodable {
let contentId: Int let contentId: Int
let coverImageUrl: String let coverImageUrl: String
@@ -55,13 +38,6 @@ struct GetAudioContentMainItem: Decodable {
let isPointAvailable: Bool let isPointAvailable: Bool
} }
struct GetAudioContentCurationResponse: Decodable {
let curationId: Int
let title: String
let description: String
let contents: [GetAudioContentMainItem]
}
struct GetAudioContentBannerResponse: Decodable { struct GetAudioContentBannerResponse: Decodable {
let type: AudioContentBannerType let type: AudioContentBannerType
let thumbnailImageUrl: String let thumbnailImageUrl: String

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +0,0 @@
//
// GetContentCurationResponse.swift
// SodaLive
//
// Created by klaus on 2/21/25.
//
struct GetContentCurationResponse: Decodable {
let title: String
let items: [GetAudioContentMainItem]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,35 +5,7 @@
// Created by klaus on 2/20/25. // 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 { struct GetSeriesGenreListResponse: Decodable {
let id: Int let id: Int
let genre: String 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]
}

View File

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

View File

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

View File

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

View File

@@ -9,11 +9,11 @@ import Foundation
import Moya import Moya
enum SeriesMainApi { enum SeriesMainApi {
case fetchHome(isAdultContentVisible: Bool, contentType: ContentType) case fetchHome
case getRecommendSeriesList(isAdultContentVisible: Bool, contentType: ContentType) case getRecommendSeriesList
case getDayOfWeekSeriesList(dayOfWeek: SeriesPublishedDaysOfWeek, isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int) case getDayOfWeekSeriesList(dayOfWeek: SeriesPublishedDaysOfWeek, page: Int, size: Int)
case getGenreList(isAdultContentVisible: Bool, contentType: ContentType) case getGenreList
case getSeriesListByGenre(genreId: Int, isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int) case getSeriesListByGenre(genreId: Int, page: Int, size: Int)
} }
extension SeriesMainApi: TargetType { extension SeriesMainApi: TargetType {
@@ -39,46 +39,27 @@ extension SeriesMainApi: TargetType {
var task: Moya.Task { var task: Moya.Task {
switch self { switch self {
case .fetchHome(let isAdultContentVisible, let contentType): case .fetchHome:
let parameters = [ return .requestPlain
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType,
] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) case .getRecommendSeriesList:
return .requestPlain
case .getRecommendSeriesList(let isAdultContentVisible, let contentType): case .getDayOfWeekSeriesList(let dayOfWeek, let page, let size):
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):
let parameters = [ let parameters = [
"dayOfWeek": dayOfWeek, "dayOfWeek": dayOfWeek,
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType,
"page": page - 1, "page": page - 1,
"size": size "size": size
] as [String : Any] ] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .getGenreList(let isAdultContentVisible, let contentType): case .getGenreList:
let parameters = [ return .requestPlain
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType,
] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) case .getSeriesListByGenre(let genreId, let page, let size):
case .getSeriesListByGenre(let genreId, let isAdultContentVisible, let contentType, let page, let size):
let parameters = [ let parameters = [
"genreId": genreId, "genreId": genreId,
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType,
"page": page - 1, "page": page - 1,
"size": size "size": size
] as [String : Any] ] as [String : Any]

View File

@@ -14,29 +14,17 @@ class SeriesMainRepository {
private let api = MoyaProvider<SeriesMainApi>() private let api = MoyaProvider<SeriesMainApi>()
func fetchHome() -> AnyPublisher<Response, MoyaError> { func fetchHome() -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher( return api.requestPublisher(.fetchHome)
.fetchHome(
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
)
)
} }
func getRecommendSeriesList() -> AnyPublisher<Response, MoyaError> { func getRecommendSeriesList() -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher( return api.requestPublisher(.getRecommendSeriesList)
.getRecommendSeriesList(
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
)
)
} }
func getDayOfWeekSeriesList(dayOfWeek: SeriesPublishedDaysOfWeek, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> { func getDayOfWeekSeriesList(dayOfWeek: SeriesPublishedDaysOfWeek, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher( return api.requestPublisher(
.getDayOfWeekSeriesList( .getDayOfWeekSeriesList(
dayOfWeek: dayOfWeek, dayOfWeek: dayOfWeek,
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
page: page, page: page,
size: size size: size
) )
@@ -44,20 +32,13 @@ class SeriesMainRepository {
} }
func getGenreList() -> AnyPublisher<Response, MoyaError> { func getGenreList() -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher( return api.requestPublisher(.getGenreList)
.getGenreList(
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
)
)
} }
func getSeriesListByGenre(genreId: Int, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> { func getSeriesListByGenre(genreId: Int, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher( return api.requestPublisher(
.getSeriesListByGenre( .getSeriesListByGenre(
genreId: genreId, genreId: genreId,
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
page: page, page: page,
size: size size: size
) )

View File

@@ -9,10 +9,10 @@ import Foundation
import Moya import Moya
enum SeriesApi { enum SeriesApi {
case getSeriesList(creatorId: Int?, isOriginal: Bool, isCompleted: Bool, sortType: SeriesListAllViewModel.SeriesSortType, isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int) case getSeriesList(creatorId: Int?, isOriginal: Bool, isCompleted: Bool, sortType: SeriesListAllViewModel.SeriesSortType, page: Int, size: Int)
case getSeriesDetail(seriesId: Int, isAdultContentVisible: Bool) case getSeriesDetail(seriesId: Int)
case getSeriesContentList(seriesId: Int, isAdultContentVisible: Bool, page: Int, size: Int, sortType: SeriesListAllViewModel.SeriesSortType) case getSeriesContentList(seriesId: Int, page: Int, size: Int, sortType: SeriesListAllViewModel.SeriesSortType)
case getRecommendSeriesList(isAdultContentVisible: Bool, contentType: ContentType) case getRecommendSeriesList
} }
extension SeriesApi: TargetType { extension SeriesApi: TargetType {
@@ -25,10 +25,10 @@ extension SeriesApi: TargetType {
case .getSeriesList: case .getSeriesList:
return "/audio-content/series" return "/audio-content/series"
case .getSeriesDetail(let seriesId, _): case .getSeriesDetail(let seriesId):
return "/audio-content/series/\(seriesId)" return "/audio-content/series/\(seriesId)"
case .getSeriesContentList(let seriesId, _, _, _, _): case .getSeriesContentList(let seriesId, _, _, _):
return "/audio-content/series/\(seriesId)/content" return "/audio-content/series/\(seriesId)/content"
case .getRecommendSeriesList: case .getRecommendSeriesList:
@@ -45,11 +45,9 @@ extension SeriesApi: TargetType {
var task: Moya.Task { var task: Moya.Task {
switch self { 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 = [ var parameters = [
"sortType": sortType, "sortType": sortType,
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType,
"isOriginal": isOriginal, "isOriginal": isOriginal,
"isCompleted": isCompleted, "isCompleted": isCompleted,
"page": page - 1, "page": page - 1,
@@ -62,21 +60,14 @@ extension SeriesApi: TargetType {
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .getSeriesDetail(_, let isAdultContentVisible): case .getSeriesDetail:
let parameters = ["isAdultContentVisible": isAdultContentVisible] as [String : Any] return .requestPlain
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .getRecommendSeriesList(let isAdultContentVisible, let contentType): case .getRecommendSeriesList:
return .requestPlain
case .getSeriesContentList(_, let page, let size, let sortType):
let parameters = [ 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, "page": page - 1,
"size": size, "size": size,
"sortType": sortType "sortType": sortType

View File

@@ -20,8 +20,6 @@ class SeriesRepository {
isOriginal: isOriginal, isOriginal: isOriginal,
isCompleted: isCompleted, isCompleted: isCompleted,
sortType: sortType, sortType: sortType,
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
page: page, page: page,
size: size size: size
) )
@@ -29,19 +27,13 @@ class SeriesRepository {
} }
func getSeriesDetail(seriesId: Int) -> AnyPublisher<Response, MoyaError> { func getSeriesDetail(seriesId: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher( return api.requestPublisher(.getSeriesDetail(seriesId: seriesId))
.getSeriesDetail(
seriesId: seriesId,
isAdultContentVisible: UserDefaults.isAdultContentVisible()
)
)
} }
func getSeriesContentList(seriesId: Int, page: Int, size: Int, sortType: SeriesListAllViewModel.SeriesSortType) -> AnyPublisher<Response, MoyaError> { func getSeriesContentList(seriesId: Int, page: Int, size: Int, sortType: SeriesListAllViewModel.SeriesSortType) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher( return api.requestPublisher(
.getSeriesContentList( .getSeriesContentList(
seriesId: seriesId, seriesId: seriesId,
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
page: page, page: page,
size: size, size: size,
sortType: sortType sortType: sortType
@@ -50,11 +42,6 @@ class SeriesRepository {
} }
func getRecommendSeriesList() -> AnyPublisher<Response, MoyaError> { func getRecommendSeriesList() -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher( return api.requestPublisher(.getRecommendSeriesList)
.getRecommendSeriesList(
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
)
)
} }
} }

View File

@@ -290,24 +290,6 @@ struct AppStepLayerView: View {
case .search: case .search:
SearchView() 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: case .message:
MessageView() MessageView()

View File

@@ -22,6 +22,10 @@ struct ChatTextFieldView: UIViewRepresentable {
return true return true
} }
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
return parent.isEnabled
}
@objc func textDidChange(_ textField: UITextField) { @objc func textDidChange(_ textField: UITextField) {
parent.text = textField.text ?? "" parent.text = textField.text ?? ""
} }
@@ -29,6 +33,7 @@ struct ChatTextFieldView: UIViewRepresentable {
@Binding var text: String @Binding var text: String
var placeholder: String var placeholder: String
var isEnabled: Bool = true
var onSend: () -> Void var onSend: () -> Void
func makeUIView(context: Context) -> UITextField { func makeUIView(context: Context) -> UITextField {
@@ -44,6 +49,7 @@ struct ChatTextFieldView: UIViewRepresentable {
textField.tintColor = UIColor(hex: "3BB9F1") textField.tintColor = UIColor(hex: "3BB9F1")
textField.font = UIFont(name: Font.preMedium.rawValue, size: 13.3) textField.font = UIFont(name: Font.preMedium.rawValue, size: 13.3)
textField.returnKeyType = .send textField.returnKeyType = .send
textField.isEnabled = isEnabled
textField.setContentHuggingPriority(.defaultLow, for: .horizontal) // textField.setContentHuggingPriority(.defaultLow, for: .horizontal) //
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) // textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) //
textField.addTarget(context.coordinator, action: #selector(Coordinator.textDidChange(_:)), for: .editingChanged) textField.addTarget(context.coordinator, action: #selector(Coordinator.textDidChange(_:)), for: .editingChanged)
@@ -51,7 +57,13 @@ struct ChatTextFieldView: UIViewRepresentable {
} }
func updateUIView(_ uiView: UITextField, context: Context) { func updateUIView(_ uiView: UITextField, context: Context) {
context.coordinator.parent = self
uiView.text = text uiView.text = text
uiView.isEnabled = isEnabled
if !isEnabled && uiView.isFirstResponder {
uiView.resignFirstResponder()
}
} }
func makeCoordinator() -> Coordinator { func makeCoordinator() -> Coordinator {

View File

@@ -12,7 +12,7 @@ enum ExplorerApi {
case getCreatorRank case getCreatorRank
case getExplorer case getExplorer
case searchChannel(channel: String) case searchChannel(channel: String)
case getCreatorProfile(userId: Int, isAdultContentVisible: Bool) case getCreatorProfile(userId: Int)
case getCreatorDetail(userId: Int) case getCreatorDetail(userId: Int)
case getFollowerList(userId: Int, page: Int, size: Int) case getFollowerList(userId: Int, page: Int, size: Int)
case getCreatorProfileCheers(userId: Int, page: Int, size: Int) case getCreatorProfileCheers(userId: Int, page: Int, size: Int)
@@ -40,7 +40,7 @@ extension ExplorerApi: TargetType {
case .searchChannel: case .searchChannel:
return "/explorer/search/channel" return "/explorer/search/channel"
case .getCreatorProfile(let userId, _): case .getCreatorProfile(let userId):
return "/explorer/profile/\(userId)" return "/explorer/profile/\(userId)"
case .getCreatorDetail(let userId): case .getCreatorDetail(let userId):
@@ -90,8 +90,8 @@ extension ExplorerApi: TargetType {
case .searchChannel(let channel): case .searchChannel(let channel):
return .requestParameters(parameters: ["channel" : channel], encoding: URLEncoding.queryString) return .requestParameters(parameters: ["channel" : channel], encoding: URLEncoding.queryString)
case .getCreatorProfile(_, let isAdultContentVisible): case .getCreatorProfile:
let parameters = ["isAdultContentVisible": isAdultContentVisible, "timezone": TimeZone.current.identifier] as [String: Any] let parameters = ["timezone": TimeZone.current.identifier] as [String: Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .getFollowerList(_, let page, let size): case .getFollowerList(_, let page, let size):

View File

@@ -22,12 +22,7 @@ final class ExplorerRepository {
} }
func getCreatorProfile(id: Int) -> AnyPublisher<Response, MoyaError> { func getCreatorProfile(id: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher( return api.requestPublisher(.getCreatorProfile(userId: id))
.getCreatorProfile(
userId: id,
isAdultContentVisible: UserDefaults.isAdultContentVisible()
)
)
} }
func getCreatorDetail(id: Int) -> AnyPublisher<Response, MoyaError> { func getCreatorDetail(id: Int) -> AnyPublisher<Response, MoyaError> {

View File

@@ -25,6 +25,7 @@ enum UserDefaultsKey: String, CaseIterable {
case notShowingEventPopupId case notShowingEventPopupId
case isAdultContentVisible case isAdultContentVisible
case contentPreference case contentPreference
case countryCode
case isAuditionNotification case isAuditionNotification
case searchChannel case searchChannel
case marketingPid case marketingPid
@@ -73,7 +74,7 @@ extension UserDefaults {
static func isAdultContentVisible() -> Bool { static func isAdultContentVisible() -> Bool {
let key = UserDefaultsKey.isAdultContentVisible.rawValue 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() { static func reset() {

View File

@@ -9,11 +9,11 @@ import Foundation
import Moya import Moya
enum HomeApi { enum HomeApi {
case getHomeData(isAdultContentVisible: Bool, contentType: ContentType) case getHomeData
case getLatestContentByTheme(theme: String, isAdultContentVisible: Bool, contentType: ContentType) case getLatestContentByTheme(theme: String)
case getDayOfWeekSeriesList(dayOfWeek: SeriesPublishedDaysOfWeek, isAdultContentVisible: Bool, contentType: ContentType) case getDayOfWeekSeriesList(dayOfWeek: SeriesPublishedDaysOfWeek)
case getRecommendContents(isAdultContentVisible: Bool, contentType: ContentType) case getRecommendContents
case getContentRankingBySort(sort: ContentRankingSortType, isAdultContentVisible: Bool, contentType: ContentType) case getContentRankingBySort(sort: ContentRankingSortType)
} }
extension HomeApi: TargetType { extension HomeApi: TargetType {
@@ -46,46 +46,33 @@ extension HomeApi: TargetType {
var task: Moya.Task { var task: Moya.Task {
switch self { switch self {
case .getHomeData(let isAdultContentVisible, let contentType): case .getHomeData:
let parameters = [ let parameters = [
"timezone": TimeZone.current.identifier, "timezone": TimeZone.current.identifier
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType
] as [String: Any] ] as [String: Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .getLatestContentByTheme(let theme, let isAdultContentVisible, let contentType): case .getLatestContentByTheme(let theme):
let parameters = [ let parameters = [
"theme": theme, "theme": theme
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType
] as [String: Any] ] as [String: Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .getDayOfWeekSeriesList(let dayOfWeek, let isAdultContentVisible, let contentType): case .getDayOfWeekSeriesList(let dayOfWeek):
let parameters = [ let parameters = [
"dayOfWeek": dayOfWeek, "dayOfWeek": dayOfWeek
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType
] as [String: Any] ] as [String: Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .getRecommendContents(let isAdultContentVisible, let contentType): case .getRecommendContents:
let parameters = [ return .requestPlain
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType
] as [String: Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) case .getContentRankingBySort(let sort):
case .getContentRankingBySort(let sort, let isAdultContentVisible, let contentType):
let parameters = [ let parameters = [
"sort": sort, "sort": sort
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType
] as [String: Any] ] as [String: Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)

View File

@@ -14,50 +14,22 @@ class HomeTabRepository {
private let api = MoyaProvider<HomeApi>() private let api = MoyaProvider<HomeApi>()
func fetchData() -> AnyPublisher<Response, MoyaError> { func fetchData() -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher( return api.requestPublisher(.getHomeData)
.getHomeData(
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
)
)
} }
func getLatestContentByTheme(theme: String) -> AnyPublisher<Response, MoyaError> { func getLatestContentByTheme(theme: String) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher( return api.requestPublisher(.getLatestContentByTheme(theme: theme))
.getLatestContentByTheme(
theme: theme,
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
)
)
} }
func getDayOfWeekSeriesList(dayOfWeek: SeriesPublishedDaysOfWeek) -> AnyPublisher<Response, MoyaError> { func getDayOfWeekSeriesList(dayOfWeek: SeriesPublishedDaysOfWeek) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher( return api.requestPublisher(.getDayOfWeekSeriesList(dayOfWeek: dayOfWeek))
.getDayOfWeekSeriesList(
dayOfWeek: dayOfWeek,
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
)
)
} }
func getRecommendContents() -> AnyPublisher<Response, MoyaError> { func getRecommendContents() -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher( return api.requestPublisher(.getRecommendContents)
.getRecommendContents(
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
)
)
} }
func getContentRankingBySort(sort: ContentRankingSortType) -> AnyPublisher<Response, MoyaError> { func getContentRankingBySort(sort: ContentRankingSortType) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher( return api.requestPublisher(.getContentRankingBySort(sort: sort))
.getContentRankingBySort(
sort: sort,
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
)
)
} }
} }

View File

@@ -44,7 +44,14 @@ struct HomeTabView: View {
AppState.shared.setAppStep(step: .login) AppState.shared.setAppStep(step: .login)
return 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 = { pendingAction = {
AppState.shared AppState.shared
.setAppStep(step: .characterDetail(characterId: characterId)) .setAppStep(step: .characterDetail(characterId: characterId))
@@ -52,6 +59,15 @@ struct HomeTabView: View {
isShowAuthConfirmView = true isShowAuthConfirmView = true
return 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)) AppState.shared.setAppStep(step: .characterDetail(characterId: characterId))
} }

View File

@@ -408,6 +408,30 @@ enum I18n {
// //
static var alertTitle: String { pick(ko: "알림", en: "Notice", ja: "お知らせ") } 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 { static var logoutQuestion: String {
pick( pick(
@@ -773,6 +797,8 @@ enum I18n {
static var signatureOn: String { pick(ko: "시그 ON", en: "Sign ON", ja: "シグ ON") } 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 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 captionOn: String { pick(ko: "자막 ON", en: "Caption ON", ja: "字幕 ON") }
static var captionOff: String { pick(ko: "자막 OFF", en: "Caption OFF", ja: "字幕 OFF") } static var captionOff: String { pick(ko: "자막 OFF", en: "Caption OFF", ja: "字幕 OFF") }
static var backgroundOn: String { pick(ko: "배경 ON", en: "Back ON", ja: "背景 ON") } 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 participants: String { pick(ko: "참여자", en: "Participants", ja: "リスナー") }
static var follow: String { pick(ko: "팔로우", en: "Follow", ja: "フォロー") } static var follow: String { pick(ko: "팔로우", en: "Follow", ja: "フォロー") }
static var following: String { pick(ko: "팔로잉", en: "Following", 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 { enum LiveNow {

View File

@@ -30,6 +30,7 @@ enum LiveApi {
case setListener(request: SetManagerOrSpeakerOrAudienceRequest) case setListener(request: SetManagerOrSpeakerOrAudienceRequest)
case setSpeaker(request: SetManagerOrSpeakerOrAudienceRequest) case setSpeaker(request: SetManagerOrSpeakerOrAudienceRequest)
case setManager(request: SetManagerOrSpeakerOrAudienceRequest) case setManager(request: SetManagerOrSpeakerOrAudienceRequest)
case setChatFreeze(request: SetChatFreezeRequest)
case kickOut(request: LiveRoomKickOutRequest) case kickOut(request: LiveRoomKickOutRequest)
case donationStatus(roomId: Int) case donationStatus(roomId: Int)
case donationTotal(roomId: Int) case donationTotal(roomId: Int)
@@ -40,7 +41,7 @@ enum LiveApi {
case likeHeart(request: LiveRoomLikeHeartRequest) case likeHeart(request: LiveRoomLikeHeartRequest)
case getTotalHeartCount(roomId: Int) case getTotalHeartCount(roomId: Int)
case heartStatus(roomId: Int) case heartStatus(roomId: Int)
case getLiveMain(isAdultContentVisible: Bool, contentType: ContentType) case getLiveMain
} }
extension LiveApi: TargetType { extension LiveApi: TargetType {
@@ -113,6 +114,9 @@ extension LiveApi: TargetType {
case .setManager: case .setManager:
return "/live/room/info/set/manager" return "/live/room/info/set/manager"
case .setChatFreeze:
return "/live/room/info/set/chat-freeze"
case .kickOut: case .kickOut:
return "/live/room/kick-out" return "/live/room/kick-out"
@@ -156,7 +160,7 @@ extension LiveApi: TargetType {
case .makeReservation, .enterRoom, .createRoom, .quitRoom, .donation, .refundDonation, .kickOut, .likeHeart: case .makeReservation, .enterRoom, .createRoom, .quitRoom, .donation, .refundDonation, .kickOut, .likeHeart:
return .post return .post
case .setListener, .setSpeaker, .setManager, .cancelReservation, .startLive, .cancelRoom, .editLiveRoomInfo: case .setListener, .setSpeaker, .setManager, .setChatFreeze, .cancelReservation, .startLive, .cancelRoom, .editLiveRoomInfo:
return .put return .put
case .deleteDonationMessage: case .deleteDonationMessage:
@@ -238,6 +242,9 @@ extension LiveApi: TargetType {
case .setListener(let request), .setSpeaker(let request), .setManager(let request): case .setListener(let request), .setSpeaker(let request), .setManager(let request):
return .requestJSONEncodable(request) return .requestJSONEncodable(request)
case .setChatFreeze(let request):
return .requestJSONEncodable(request)
case .kickOut(let request): case .kickOut(let request):
return .requestJSONEncodable(request) return .requestJSONEncodable(request)
@@ -253,11 +260,9 @@ extension LiveApi: TargetType {
case .likeHeart(let request): case .likeHeart(let request):
return .requestJSONEncodable(request) return .requestJSONEncodable(request)
case .getLiveMain(let isAdultContentVisible, let contentType): case .getLiveMain:
let parameters = [ let parameters = [
"timezone": TimeZone.current.identifier, "timezone": TimeZone.current.identifier
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType
] as [String: Any] ] as [String: Any]
return .requestParameters( return .requestParameters(

View File

@@ -93,6 +93,10 @@ final class LiveRepository {
return api.requestPublisher(.setManager(request: SetManagerOrSpeakerOrAudienceRequest(roomId: roomId, memberId: userId))) 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> { func kickOut(roomId: Int, userId: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.kickOut(request: LiveRoomKickOutRequest(roomId: roomId, userId: userId))) return api.requestPublisher(.kickOut(request: LiveRoomKickOutRequest(roomId: roomId, userId: userId)))
} }
@@ -141,11 +145,6 @@ final class LiveRepository {
} }
func getLiveMain() -> AnyPublisher<Response, MoyaError> { func getLiveMain() -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher( return api.requestPublisher(.getLiveMain)
.getLiveMain(
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
)
)
} }
} }

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