diff --git a/SodaLive.xcodeproj/project.pbxproj b/SodaLive.xcodeproj/project.pbxproj index fd481884..edb012b0 100644 --- a/SodaLive.xcodeproj/project.pbxproj +++ b/SodaLive.xcodeproj/project.pbxproj @@ -7,36 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - CC0201012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift */; }; - CC0202012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift */; }; - CC0203012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift */; }; - CC0201022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift */; }; - CC0202022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift */; }; - CC0203022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift */; }; - CC0201032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift */; }; - CC0202032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift */; }; - CC0203032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift */; }; - CC01012A2FF0100000A5BBA3 /* ContentSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002A2FF0100000A5BBA3 /* ContentSort.swift */; }; - CC01022A2FF0100000A5BBA3 /* ContentSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002A2FF0100000A5BBA3 /* ContentSort.swift */; }; - CC01032A2FF0100000A5BBA3 /* ContentSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002A2FF0100000A5BBA3 /* ContentSort.swift */; }; - CC01012B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */; }; - CC01022B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */; }; - CC01032B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */; }; - CC01012C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift */; }; - CC01022C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift */; }; - CC01032C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift */; }; - CC01012D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift */; }; - CC01022D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift */; }; - CC01032D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift */; }; - CC01012E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */; }; - CC01022E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */; }; - CC01032E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */; }; - CC01012F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */; }; - CC01022F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */; }; - CC01032F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */; }; - CC0101302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift */; }; - CC0102302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift */; }; - CC0103302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift */; }; 00C796627A6758D271215EC5 /* CreatorProfileItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26881ED30292E28D2E5F84CD /* CreatorProfileItem.swift */; }; 042BEE0C973700FB5F4540CD /* I18n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826D626B7714E5C28ED31074 /* I18n.swift */; }; 0470E68C6F302A131264E3AF /* ChannelDonationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF2BFD2E09F509C44C92214 /* ChannelDonationViewModel.swift */; }; @@ -1736,6 +1706,13 @@ CC0101272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift */; }; CC0101282FF0100000A5BBA3 /* CreatorChannelTalkActionSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100282FF0100000A5BBA3 /* CreatorChannelTalkActionSection.swift */; }; CC0101292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift */; }; + CC01012A2FF0100000A5BBA3 /* ContentSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002A2FF0100000A5BBA3 /* ContentSort.swift */; }; + CC01012B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */; }; + CC01012C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift */; }; + CC01012D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift */; }; + CC01012E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */; }; + CC01012F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */; }; + CC0101302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift */; }; CC0102012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift */; }; CC0102032FF0100000A5BBA3 /* CreatorChannelHomeApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100032FF0100000A5BBA3 /* CreatorChannelHomeApi.swift */; }; CC0102042FF0100000A5BBA3 /* CreatorChannelHomeRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100042FF0100000A5BBA3 /* CreatorChannelHomeRepository.swift */; }; @@ -1762,6 +1739,13 @@ CC0102272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift */; }; CC0102282FF0100000A5BBA3 /* CreatorChannelTalkActionSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100282FF0100000A5BBA3 /* CreatorChannelTalkActionSection.swift */; }; CC0102292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift */; }; + CC01022A2FF0100000A5BBA3 /* ContentSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002A2FF0100000A5BBA3 /* ContentSort.swift */; }; + CC01022B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */; }; + CC01022C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift */; }; + CC01022D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift */; }; + CC01022E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */; }; + CC01022F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */; }; + CC0102302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift */; }; CC0103012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift */; }; CC0103032FF0100000A5BBA3 /* CreatorChannelHomeApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100032FF0100000A5BBA3 /* CreatorChannelHomeApi.swift */; }; CC0103042FF0100000A5BBA3 /* CreatorChannelHomeRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100042FF0100000A5BBA3 /* CreatorChannelHomeRepository.swift */; }; @@ -1788,6 +1772,43 @@ CC0103272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift */; }; CC0103282FF0100000A5BBA3 /* CreatorChannelTalkActionSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100282FF0100000A5BBA3 /* CreatorChannelTalkActionSection.swift */; }; CC0103292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift */; }; + CC01032A2FF0100000A5BBA3 /* ContentSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002A2FF0100000A5BBA3 /* ContentSort.swift */; }; + CC01032B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */; }; + CC01032C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift */; }; + CC01032D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift */; }; + CC01032E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */; }; + CC01032F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */; }; + CC0103302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift */; }; + CC0201012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift */; }; + CC0201022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift */; }; + CC0201032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift */; }; + CC0202012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift */; }; + CC0202022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift */; }; + CC0202032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift */; }; + CC0203012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift */; }; + CC0203022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift */; }; + CC0203032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift */; }; + CC0301012FF0300000A5BBA3 /* CreatorChannelAudioTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0300012FF0300000A5BBA3 /* CreatorChannelAudioTabResponse.swift */; }; + CC0301022FF0300000A5BBA3 /* CreatorChannelAudioApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0300022FF0300000A5BBA3 /* CreatorChannelAudioApi.swift */; }; + CC0301032FF0300000A5BBA3 /* CreatorChannelAudioRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0300032FF0300000A5BBA3 /* CreatorChannelAudioRepository.swift */; }; + CC0301042FF0300000A5BBA3 /* CreatorChannelAudioViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0300042FF0300000A5BBA3 /* CreatorChannelAudioViewModel.swift */; }; + CC0301052FF0300000A5BBA3 /* CreatorChannelAudioOwnershipRateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0300052FF0300000A5BBA3 /* CreatorChannelAudioOwnershipRateView.swift */; }; + CC0301062FF0300000A5BBA3 /* CreatorChannelAudioUploadButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0300062FF0300000A5BBA3 /* CreatorChannelAudioUploadButton.swift */; }; + CC0301072FF0300000A5BBA3 /* CreatorChannelAudioTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0300072FF0300000A5BBA3 /* CreatorChannelAudioTabView.swift */; }; + CC0302012FF0300000A5BBA3 /* CreatorChannelAudioTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0300012FF0300000A5BBA3 /* CreatorChannelAudioTabResponse.swift */; }; + CC0302022FF0300000A5BBA3 /* CreatorChannelAudioApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0300022FF0300000A5BBA3 /* CreatorChannelAudioApi.swift */; }; + CC0302032FF0300000A5BBA3 /* CreatorChannelAudioRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0300032FF0300000A5BBA3 /* CreatorChannelAudioRepository.swift */; }; + CC0302042FF0300000A5BBA3 /* CreatorChannelAudioViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0300042FF0300000A5BBA3 /* CreatorChannelAudioViewModel.swift */; }; + CC0302052FF0300000A5BBA3 /* CreatorChannelAudioOwnershipRateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0300052FF0300000A5BBA3 /* CreatorChannelAudioOwnershipRateView.swift */; }; + CC0302062FF0300000A5BBA3 /* CreatorChannelAudioUploadButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0300062FF0300000A5BBA3 /* CreatorChannelAudioUploadButton.swift */; }; + CC0302072FF0300000A5BBA3 /* CreatorChannelAudioTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0300072FF0300000A5BBA3 /* CreatorChannelAudioTabView.swift */; }; + CC0303012FF0300000A5BBA3 /* CreatorChannelAudioTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0300012FF0300000A5BBA3 /* CreatorChannelAudioTabResponse.swift */; }; + CC0303022FF0300000A5BBA3 /* CreatorChannelAudioApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0300022FF0300000A5BBA3 /* CreatorChannelAudioApi.swift */; }; + CC0303032FF0300000A5BBA3 /* CreatorChannelAudioRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0300032FF0300000A5BBA3 /* CreatorChannelAudioRepository.swift */; }; + CC0303042FF0300000A5BBA3 /* CreatorChannelAudioViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0300042FF0300000A5BBA3 /* CreatorChannelAudioViewModel.swift */; }; + CC0303052FF0300000A5BBA3 /* CreatorChannelAudioOwnershipRateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0300052FF0300000A5BBA3 /* CreatorChannelAudioOwnershipRateView.swift */; }; + CC0303062FF0300000A5BBA3 /* CreatorChannelAudioUploadButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0300062FF0300000A5BBA3 /* CreatorChannelAudioUploadButton.swift */; }; + CC0303072FF0300000A5BBA3 /* CreatorChannelAudioTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0300072FF0300000A5BBA3 /* CreatorChannelAudioTabView.swift */; }; D1C65856889B16DFDB69A004 /* PushNotificationRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDDF6BBD0192819B1F5B3FE /* PushNotificationRepository.swift */; }; D2A268C7346E84E6B455E25A /* Pods_SodaLive_dev.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F4E5BB7961B7E6BB68C3379 /* Pods_SodaLive_dev.framework */; }; DD202E48D61F1CE8108AD164 /* LineSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 6DBA0C325ACEC3FCDB22490D /* LineSDK */; }; @@ -1810,16 +1831,6 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - CC0200012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelAudioContentListItem.swift; sourceTree = ""; }; - CC0200022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveStartButton.swift; sourceTree = ""; }; - CC0200032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveTabView.swift; sourceTree = ""; }; - CC01002A2FF0100000A5BBA3 /* ContentSort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSort.swift; sourceTree = ""; }; - CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelSortBar.swift; sourceTree = ""; }; - CC01002C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelSortBottomSheet.swift; sourceTree = ""; }; - CC01002D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveTabResponse.swift; sourceTree = ""; }; - CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveApi.swift; sourceTree = ""; }; - CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveRepository.swift; sourceTree = ""; }; - CC0100302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveViewModel.swift; sourceTree = ""; }; 0BBDC3ECBBE049EAA4057D3A /* PostChannelDonationRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PostChannelDonationRequest.swift; sourceTree = ""; }; 0EC05859619BDC03056911E7 /* MainTab.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainTab.swift; sourceTree = ""; }; 189350EA101CF12EEBA6EC1D /* MainTabBarView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainTabBarView.swift; sourceTree = ""; }; @@ -2657,6 +2668,23 @@ CC0100272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelSnsSection.swift; sourceTree = ""; }; CC0100282FF0100000A5BBA3 /* CreatorChannelTalkActionSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelTalkActionSection.swift; sourceTree = ""; }; CC0100292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelFloatingActionMenu.swift; sourceTree = ""; }; + CC01002A2FF0100000A5BBA3 /* ContentSort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSort.swift; sourceTree = ""; }; + CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelSortBar.swift; sourceTree = ""; }; + CC01002C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelSortBottomSheet.swift; sourceTree = ""; }; + CC01002D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveTabResponse.swift; sourceTree = ""; }; + CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveApi.swift; sourceTree = ""; }; + CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveRepository.swift; sourceTree = ""; }; + CC0100302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveViewModel.swift; sourceTree = ""; }; + CC0200012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelAudioContentListItem.swift; sourceTree = ""; }; + CC0200022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveStartButton.swift; sourceTree = ""; }; + CC0200032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveTabView.swift; sourceTree = ""; }; + CC0300012FF0300000A5BBA3 /* CreatorChannelAudioTabResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelAudioTabResponse.swift; sourceTree = ""; }; + CC0300022FF0300000A5BBA3 /* CreatorChannelAudioApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelAudioApi.swift; sourceTree = ""; }; + CC0300032FF0300000A5BBA3 /* CreatorChannelAudioRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelAudioRepository.swift; sourceTree = ""; }; + CC0300042FF0300000A5BBA3 /* CreatorChannelAudioViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelAudioViewModel.swift; sourceTree = ""; }; + CC0300052FF0300000A5BBA3 /* CreatorChannelAudioOwnershipRateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelAudioOwnershipRateView.swift; sourceTree = ""; }; + CC0300062FF0300000A5BBA3 /* CreatorChannelAudioUploadButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelAudioUploadButton.swift; sourceTree = ""; }; + CC0300072FF0300000A5BBA3 /* CreatorChannelAudioTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelAudioTabView.swift; sourceTree = ""; }; CC73FFC54CB1D988053059D1 /* LanguageEnvironment.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LanguageEnvironment.swift; sourceTree = ""; }; CE82912F2F111593BE42E33C /* CommunityPostCard.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommunityPostCard.swift; sourceTree = ""; }; D03CBA8005C81F7C31F59A89 /* Pods-SodaLive.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SodaLive.release.xcconfig"; path = "Target Support Files/Pods-SodaLive/Pods-SodaLive.release.xcconfig"; sourceTree = ""; }; @@ -2744,43 +2772,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - CC0200042FF0200000A5BBA3 /* Components */ = { - isa = PBXGroup; - children = ( - CC0200022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift */, - ); - path = Components; - sourceTree = ""; - }; - CC0100312FF0100000A5BBA3 /* Models */ = { - isa = PBXGroup; - children = ( - CC01002D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift */, - ); - path = Models; - sourceTree = ""; - }; - CC0100322FF0100000A5BBA3 /* Repository */ = { - isa = PBXGroup; - children = ( - CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */, - CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */, - ); - path = Repository; - sourceTree = ""; - }; - CC0100332FF0100000A5BBA3 /* Live */ = { - isa = PBXGroup; - children = ( - CC0100312FF0100000A5BBA3 /* Models */, - CC0100322FF0100000A5BBA3 /* Repository */, - CC0200042FF0200000A5BBA3 /* Components */, - CC0100302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift */, - CC0200032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift */, - ); - path = Live; - sourceTree = ""; - }; 038B15E425DE0639156ADBA9 /* Repository */ = { isa = PBXGroup; children = ( @@ -5298,6 +5289,7 @@ CC0100262FF0100000A5BBA3 /* Components */, CC0100232FF0100000A5BBA3 /* Home */, CC0100332FF0100000A5BBA3 /* Live */, + CC0300112FF0300000A5BBA3 /* Audio */, CC0100082FF0100000A5BBA3 /* CreatorChannelView.swift */, CC0100092FF0100000A5BBA3 /* CreatorChannelViewModel.swift */, ); @@ -5327,6 +5319,81 @@ path = Components; sourceTree = ""; }; + CC0100312FF0100000A5BBA3 /* Models */ = { + isa = PBXGroup; + children = ( + CC01002D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift */, + ); + path = Models; + sourceTree = ""; + }; + CC0100322FF0100000A5BBA3 /* Repository */ = { + isa = PBXGroup; + children = ( + CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */, + CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */, + ); + path = Repository; + sourceTree = ""; + }; + CC0100332FF0100000A5BBA3 /* Live */ = { + isa = PBXGroup; + children = ( + CC0100312FF0100000A5BBA3 /* Models */, + CC0100322FF0100000A5BBA3 /* Repository */, + CC0200042FF0200000A5BBA3 /* Components */, + CC0100302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift */, + CC0200032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift */, + ); + path = Live; + sourceTree = ""; + }; + CC0200042FF0200000A5BBA3 /* Components */ = { + isa = PBXGroup; + children = ( + CC0200022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift */, + ); + path = Components; + sourceTree = ""; + }; + CC0300082FF0300000A5BBA3 /* Models */ = { + isa = PBXGroup; + children = ( + CC0300012FF0300000A5BBA3 /* CreatorChannelAudioTabResponse.swift */, + ); + path = Models; + sourceTree = ""; + }; + CC0300092FF0300000A5BBA3 /* Repository */ = { + isa = PBXGroup; + children = ( + CC0300022FF0300000A5BBA3 /* CreatorChannelAudioApi.swift */, + CC0300032FF0300000A5BBA3 /* CreatorChannelAudioRepository.swift */, + ); + path = Repository; + sourceTree = ""; + }; + CC0300102FF0300000A5BBA3 /* Components */ = { + isa = PBXGroup; + children = ( + CC0300052FF0300000A5BBA3 /* CreatorChannelAudioOwnershipRateView.swift */, + CC0300062FF0300000A5BBA3 /* CreatorChannelAudioUploadButton.swift */, + ); + path = Components; + sourceTree = ""; + }; + CC0300112FF0300000A5BBA3 /* Audio */ = { + isa = PBXGroup; + children = ( + CC0300082FF0300000A5BBA3 /* Models */, + CC0300092FF0300000A5BBA3 /* Repository */, + CC0300102FF0300000A5BBA3 /* Components */, + CC0300042FF0300000A5BBA3 /* CreatorChannelAudioViewModel.swift */, + CC0300072FF0300000A5BBA3 /* CreatorChannelAudioTabView.swift */, + ); + path = Audio; + sourceTree = ""; + }; D37878F5929361B43B1E3C5C /* Card */ = { isa = PBXGroup; children = ( @@ -5787,6 +5854,13 @@ CC0201012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift in Sources */, CC0201022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift in Sources */, CC0201032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift in Sources */, + CC0301012FF0300000A5BBA3 /* CreatorChannelAudioTabResponse.swift in Sources */, + CC0301022FF0300000A5BBA3 /* CreatorChannelAudioApi.swift in Sources */, + CC0301032FF0300000A5BBA3 /* CreatorChannelAudioRepository.swift in Sources */, + CC0301042FF0300000A5BBA3 /* CreatorChannelAudioViewModel.swift in Sources */, + CC0301052FF0300000A5BBA3 /* CreatorChannelAudioOwnershipRateView.swift in Sources */, + CC0301062FF0300000A5BBA3 /* CreatorChannelAudioUploadButton.swift in Sources */, + CC0301072FF0300000A5BBA3 /* CreatorChannelAudioTabView.swift in Sources */, 409E35082B0759DE00CCB985 /* MoyaProviderExtension.swift in Sources */, 40DDBD772B2B4FD700A2E8DA /* CreatorCommunityMoreItemView.swift in Sources */, 400EA7742A89C42C004992CF /* StartLiveRequest.swift in Sources */, @@ -6609,6 +6683,13 @@ CC0202012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift in Sources */, CC0202022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift in Sources */, CC0202032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift in Sources */, + CC0302012FF0300000A5BBA3 /* CreatorChannelAudioTabResponse.swift in Sources */, + CC0302022FF0300000A5BBA3 /* CreatorChannelAudioApi.swift in Sources */, + CC0302032FF0300000A5BBA3 /* CreatorChannelAudioRepository.swift in Sources */, + CC0302042FF0300000A5BBA3 /* CreatorChannelAudioViewModel.swift in Sources */, + CC0302052FF0300000A5BBA3 /* CreatorChannelAudioOwnershipRateView.swift in Sources */, + CC0302062FF0300000A5BBA3 /* CreatorChannelAudioUploadButton.swift in Sources */, + CC0302072FF0300000A5BBA3 /* CreatorChannelAudioTabView.swift in Sources */, 401B3F092FEEF10000A5BBA3 /* MainHomeRankingViewModel.swift in Sources */, 401A3FAC2FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */, 401A3FAD2FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */, @@ -7447,6 +7528,13 @@ CC0203012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift in Sources */, CC0203022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift in Sources */, CC0203032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift in Sources */, + CC0303012FF0300000A5BBA3 /* CreatorChannelAudioTabResponse.swift in Sources */, + CC0303022FF0300000A5BBA3 /* CreatorChannelAudioApi.swift in Sources */, + CC0303032FF0300000A5BBA3 /* CreatorChannelAudioRepository.swift in Sources */, + CC0303042FF0300000A5BBA3 /* CreatorChannelAudioViewModel.swift in Sources */, + CC0303052FF0300000A5BBA3 /* CreatorChannelAudioOwnershipRateView.swift in Sources */, + CC0303062FF0300000A5BBA3 /* CreatorChannelAudioUploadButton.swift in Sources */, + CC0303072FF0300000A5BBA3 /* CreatorChannelAudioTabView.swift in Sources */, 401B3F082FEEF10000A5BBA3 /* MainHomeRankingViewModel.swift in Sources */, 401A3FB02FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */, 401A3FB12FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */, diff --git a/SodaLive/Sources/I18n/I18n.swift b/SodaLive/Sources/I18n/I18n.swift index 963463c3..792233b2 100644 --- a/SodaLive/Sources/I18n/I18n.swift +++ b/SodaLive/Sources/I18n/I18n.swift @@ -3181,6 +3181,24 @@ If you block this user, the following features will be restricted. } } + enum CreatorChannelAudio { + static var totalLabel: String { pick(ko: "전체", en: "Total", ja: "全体") } + static var countUnit: String { pick(ko: "개", en: "", ja: "件") } + static var ownershipPrefix: String { + pick(ko: "전체 오디오의 ", en: "You own ", ja: "全オーディオの") + } + static var ownershipSuffix: String { + pick(ko: "를 소장하고 있어요.", en: " of all audio.", ja: "を保有中です。") + } + static var emptyMessage: String { + pick( + ko: "크리에이터가 오디오를 준비 중입니다.\n기대해 주세요!", + en: "The creator is preparing audio.\nPlease stay tuned!", + ja: "クリエイターがオーディオを準備中です。\nお楽しみに!" + ) + } + } + enum CreatorChannelHome { static var currentLive: String { pick(ko: "현재 라이브", en: "Live now", ja: "現在ライブ") } static var latestAudio: String { pick(ko: "최신 오디오 콘텐츠", en: "Latest audio", ja: "最新オーディオ") } diff --git a/SodaLive/Sources/V2/CreatorChannel/Audio/Components/CreatorChannelAudioOwnershipRateView.swift b/SodaLive/Sources/V2/CreatorChannel/Audio/Components/CreatorChannelAudioOwnershipRateView.swift new file mode 100644 index 00000000..c1811d8f --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Audio/Components/CreatorChannelAudioOwnershipRateView.swift @@ -0,0 +1,76 @@ +import Foundation +import SwiftUI + +struct CreatorChannelAudioOwnershipRateView: View { + let purchasedAudioContentRate: Double + let purchasedAudioContentCount: Int + let paidAudioContentCount: Int + + private var progress: CGFloat { + CGFloat(max(0, min(purchasedAudioContentRate / 100, 1))) + } + + private var rateText: String { + let rounded = purchasedAudioContentRate.rounded() + if rounded == purchasedAudioContentRate { + return "\(Int(rounded))%" + } + return String(format: "%.1f%%", purchasedAudioContentRate) + } + + var body: some View { + VStack(alignment: .leading, spacing: SodaSpacing.s14) { + HStack(alignment: .center, spacing: SodaSpacing.s4) { + ( + Text(I18n.CreatorChannelAudio.ownershipPrefix) + .foregroundColor(.white) + + Text(rateText) + .foregroundColor(Color.soda400) + + Text(I18n.CreatorChannelAudio.ownershipSuffix) + .foregroundColor(.white) + ) + .appFont(size: 14, weight: .medium) + .lineLimit(1) + + Spacer(minLength: SodaSpacing.s8) + + ( + Text(purchasedAudioContentCount.comma()) + .foregroundColor(.white) + + Text("/\(paidAudioContentCount.comma())\(I18n.CreatorChannelAudio.countUnit)") + .foregroundColor(Color.gray500) + ) + .appFont(size: 14, weight: .medium) + .lineLimit(1) + } + + GeometryReader { proxy in + ZStack(alignment: .leading) { + Capsule() + .fill(Color.gray800) + + Capsule() + .fill(Color.soda400) + .frame(width: proxy.size.width * progress) + } + } + .frame(height: 4) + } + .padding(SodaSpacing.s14) + .background(Color.gray900) + .clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous)) + .padding(.horizontal, SodaSpacing.s14) + } +} + +struct CreatorChannelAudioOwnershipRateView_Previews: PreviewProvider { + static var previews: some View { + CreatorChannelAudioOwnershipRateView( + purchasedAudioContentRate: 58, + purchasedAudioContentCount: 12, + paidAudioContentCount: 43 + ) + .background(Color.black) + .previewLayout(.sizeThatFits) + } +} diff --git a/SodaLive/Sources/V2/CreatorChannel/Audio/Components/CreatorChannelAudioUploadButton.swift b/SodaLive/Sources/V2/CreatorChannel/Audio/Components/CreatorChannelAudioUploadButton.swift new file mode 100644 index 00000000..2617d082 --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Audio/Components/CreatorChannelAudioUploadButton.swift @@ -0,0 +1,41 @@ +import SwiftUI + +struct CreatorChannelAudioUploadButton: View { + let action: () -> Void + + var body: some View { + VStack { + Button(action: action) { + HStack(spacing: SodaSpacing.s6) { + Image("ic_new_upload_audio") + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + + Text(I18n.CreatorChannelHome.uploadAudioContent) + .appFont(size: 16, weight: .bold) + .foregroundColor(.white) + .lineLimit(1) + } + .frame(maxWidth: .infinity) + .frame(height: 52) + .padding(.horizontal, SodaSpacing.s14) + .background(Color.soda400) + .clipShape(Capsule()) + } + .buttonStyle(.plain) + } + .padding(.top, SodaSpacing.s14) + .padding(.bottom, 34) + .background(Color.black) + } +} + +struct CreatorChannelAudioUploadButton_Previews: PreviewProvider { + static var previews: some View { + CreatorChannelAudioUploadButton(action: {}) + .padding(SodaSpacing.s20) + .background(Color.black) + .previewLayout(.sizeThatFits) + } +} diff --git a/SodaLive/Sources/V2/CreatorChannel/Audio/CreatorChannelAudioTabView.swift b/SodaLive/Sources/V2/CreatorChannel/Audio/CreatorChannelAudioTabView.swift new file mode 100644 index 00000000..021627ec --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Audio/CreatorChannelAudioTabView.swift @@ -0,0 +1,108 @@ +import SwiftUI + +struct CreatorChannelAudioTabView: View { + let creatorId: Int + let isOwnCreatorChannel: Bool + let onTapContent: (Int) -> Void + + @StateObject private var viewModel = CreatorChannelAudioViewModel() + @State private var isSortSheetPresented = false + + var body: some View { + content + .background(Color.black) + .onAppear { + if viewModel.hasLoaded == false { + viewModel.fetchFirstPage(creatorId: creatorId) + } + } + .sheet(isPresented: $isSortSheetPresented) { + CreatorChannelSortBottomSheet(selectedSort: viewModel.selectedSort) { sort in + viewModel.selectSort(sort, creatorId: creatorId) + isSortSheetPresented = false + } + } + .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 1) + } + + private var content: some View { + VStack(spacing: 0) { + if isEmptyState { + emptyStateView + } else { + CapsuleTabBar( + items: viewModel.displayThemes, + selectedItem: Binding( + get: { viewModel.selectedTheme }, + set: { viewModel.selectTheme($0, creatorId: creatorId) } + ), + title: { $0.themeName } + ) + + CreatorChannelSortBar( + selectedSort: viewModel.selectedSort, + onTapSort: { + isSortSheetPresented = true + } + ) + + LazyVStack(spacing: 0) { + if shouldShowOwnershipRate, let response = viewModel.response { + CreatorChannelAudioOwnershipRateView( + purchasedAudioContentRate: response.purchasedAudioContentRate, + purchasedAudioContentCount: response.purchasedAudioContentCount, + paidAudioContentCount: response.paidAudioContentCount + ) + .padding(.bottom, SodaSpacing.s8) + } + + ForEach(viewModel.audioContents) { audioContent in + CreatorChannelAudioContentListItem(audioContent: audioContent) { + onTapContent(audioContent.audioContentId) + } + .onAppear { + viewModel.fetchNextPageIfNeeded(creatorId: creatorId, currentItem: audioContent) + } + } + + if viewModel.isLoadingNextPage { + ProgressView() + .padding(.vertical, SodaSpacing.s20) + } + } + .padding(.bottom, isOwnCreatorChannel ? 70 : SodaSpacing.s20) + } + } + } + + private var shouldShowOwnershipRate: Bool { + guard isOwnCreatorChannel == false else { return false } + return viewModel.isAllThemeSelected + } + + private var isEmptyState: Bool { + guard viewModel.hasLoaded, let response = viewModel.response else { return false } + return response.audioContentCount == 0 + } + + private var emptyStateView: some View { + Text(I18n.CreatorChannelAudio.emptyMessage) + .appFont(.body3) + .foregroundColor(Color.gray500) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding(.vertical, 48) + } +} + +struct CreatorChannelAudioTabView_Previews: PreviewProvider { + static var previews: some View { + CreatorChannelAudioTabView( + creatorId: 1, + isOwnCreatorChannel: false, + onTapContent: { _ in } + ) + .background(Color.black) + .previewLayout(.sizeThatFits) + } +} diff --git a/SodaLive/Sources/V2/CreatorChannel/Audio/CreatorChannelAudioViewModel.swift b/SodaLive/Sources/V2/CreatorChannel/Audio/CreatorChannelAudioViewModel.swift new file mode 100644 index 00000000..86ba396a --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Audio/CreatorChannelAudioViewModel.swift @@ -0,0 +1,173 @@ +import Foundation +import Combine + +struct CreatorChannelAudioThemeItem: Hashable { + let themeId: Int? + let themeName: String +} + +final class CreatorChannelAudioViewModel: ObservableObject { + private let repository = CreatorChannelAudioRepository() + private var subscription = Set() + private let pageSize = 20 + private var latestRequestId = 0 + + @Published var isLoading = false + @Published var isLoadingNextPage = false + @Published var response: CreatorChannelAudioTabResponse? + @Published var audioContents = [CreatorChannelAudioContentResponse]() + @Published var selectedSort: ContentSort = .latest + @Published var selectedTheme = CreatorChannelAudioThemeItem.all + @Published var page = 0 + @Published var size = 20 + @Published var hasNext = false + @Published var hasLoaded = false + @Published var errorMessage = "" + @Published var isShowPopup = false + + var displayThemes: [CreatorChannelAudioThemeItem] { + let themes = response?.themes.map { + CreatorChannelAudioThemeItem(themeId: $0.themeId, themeName: $0.themeName) + } ?? [] + + return [CreatorChannelAudioThemeItem.all] + themes + } + + var isAllThemeSelected: Bool { + selectedTheme.themeId == nil + } + + func fetchFirstPage(creatorId: Int) { + fetchAudio(creatorId: creatorId, page: 0, isNextPage: false) + } + + func fetchNextPageIfNeeded(creatorId: Int, currentItem: CreatorChannelAudioContentResponse) { + guard audioContents.last?.audioContentId == currentItem.audioContentId else { return } + guard hasNext, isLoadingNextPage == false, isLoading == false else { return } + + fetchAudio(creatorId: creatorId, page: page + 1, isNextPage: true) + } + + func selectSort(_ sort: ContentSort, creatorId: Int) { + guard selectedSort != sort else { return } + + selectedSort = sort + fetchFirstPage(creatorId: creatorId) + } + + func selectTheme(_ theme: CreatorChannelAudioThemeItem, creatorId: Int) { + guard selectedTheme != theme else { return } + + selectedTheme = theme + fetchFirstPage(creatorId: creatorId) + } + + private func fetchAudio(creatorId: Int, page requestPage: Int, isNextPage: Bool) { + latestRequestId += 1 + let requestId = latestRequestId + + if isNextPage { + isLoadingNextPage = true + } else { + isLoading = true + } + + repository.getAudio( + creatorId: creatorId, + page: requestPage, + size: pageSize, + sort: selectedSort, + themeId: selectedTheme.themeId + ) + .sink { [weak self] result in + guard let self else { return } + guard requestId == self.latestRequestId else { return } + + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + self.applyFailureState() + } + } receiveValue: { [weak self] response in + guard let self else { return } + guard requestId == self.latestRequestId else { return } + + var shouldFetchNextPage = false + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse.self, from: response.data) + + if let data = decoded.data, decoded.success { + shouldFetchNextPage = self.applySuccessState(data, isNextPage: isNextPage) + } else { + self.errorMessage = decoded.message ?? I18n.Common.commonError + self.isShowPopup = true + } + } catch { + ERROR_LOG(error.localizedDescription) + self.errorMessage = I18n.Common.commonError + self.isShowPopup = true + } + + self.hasLoaded = true + self.isLoading = false + self.isLoadingNextPage = false + + if shouldFetchNextPage { + self.fetchAudio(creatorId: creatorId, page: self.page + 1, isNextPage: true) + } + } + .store(in: &subscription) + } + + private func applySuccessState(_ data: CreatorChannelAudioTabResponse, isNextPage: Bool) -> Bool { + response = data + selectedSort = data.sort + selectedTheme = selectedTheme(from: data) + page = data.page + size = data.size + hasNext = data.hasNext + + let visibleAudioContents = data.audioContents.filter { + guard let duration = $0.duration?.trimmingCharacters(in: .whitespacesAndNewlines) else { return false } + return duration.isEmpty == false + } + + if isNextPage { + audioContents.append(contentsOf: visibleAudioContents) + } else { + audioContents = visibleAudioContents + } + + return visibleAudioContents.isEmpty && data.hasNext + } + + private func selectedTheme(from data: CreatorChannelAudioTabResponse) -> CreatorChannelAudioThemeItem { + guard let themeId = data.themeId else { + return .all + } + + if let theme = data.themes.first(where: { $0.themeId == themeId }) { + return CreatorChannelAudioThemeItem(themeId: theme.themeId, themeName: theme.themeName) + } + + return CreatorChannelAudioThemeItem(themeId: themeId, themeName: selectedTheme.themeName) + } + + private func applyFailureState() { + errorMessage = I18n.Common.commonError + isShowPopup = true + hasLoaded = true + isLoading = false + isLoadingNextPage = false + } +} + +private extension CreatorChannelAudioThemeItem { + static var all: CreatorChannelAudioThemeItem { + CreatorChannelAudioThemeItem(themeId: nil, themeName: I18n.CreatorChannelAudio.totalLabel) + } +} diff --git a/SodaLive/Sources/V2/CreatorChannel/Audio/Models/CreatorChannelAudioTabResponse.swift b/SodaLive/Sources/V2/CreatorChannel/Audio/Models/CreatorChannelAudioTabResponse.swift new file mode 100644 index 00000000..3dfb2c91 --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Audio/Models/CreatorChannelAudioTabResponse.swift @@ -0,0 +1,20 @@ +import Foundation + +struct CreatorChannelAudioTabResponse: Decodable { + let audioContentCount: Int + let paidAudioContentCount: Int + let purchasedAudioContentCount: Int + let purchasedAudioContentRate: Double + let themes: [CreatorChannelAudioThemeResponse] + let audioContents: [CreatorChannelAudioContentResponse] + let sort: ContentSort + let themeId: Int? + let page: Int + let size: Int + let hasNext: Bool +} + +struct CreatorChannelAudioThemeResponse: Decodable, Hashable { + let themeId: Int + let themeName: String +} diff --git a/SodaLive/Sources/V2/CreatorChannel/Audio/Repository/CreatorChannelAudioApi.swift b/SodaLive/Sources/V2/CreatorChannel/Audio/Repository/CreatorChannelAudioApi.swift new file mode 100644 index 00000000..7fdfbfdc --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Audio/Repository/CreatorChannelAudioApi.swift @@ -0,0 +1,48 @@ +import Foundation + +import Moya + +enum CreatorChannelAudioApi { + case getAudio(creatorId: Int, page: Int, size: Int, sort: ContentSort, themeId: Int?) +} + +extension CreatorChannelAudioApi: TargetType { + var baseURL: URL { + return URL(string: BASE_URL)! + } + + var path: String { + switch self { + case .getAudio(let creatorId, _, _, _, _): + return "/api/v2/creator-channels/\(creatorId)/audio" + } + } + + var method: Moya.Method { + switch self { + case .getAudio: + return .get + } + } + + var task: Moya.Task { + switch self { + case .getAudio(_, let page, let size, let sort, let themeId): + var parameters: [String: Any] = [ + "page": page, + "size": size, + "sort": sort.rawValue + ] + + if let themeId { + parameters["themeId"] = themeId + } + + return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) + } + } + + var headers: [String: String]? { + return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"] + } +} diff --git a/SodaLive/Sources/V2/CreatorChannel/Audio/Repository/CreatorChannelAudioRepository.swift b/SodaLive/Sources/V2/CreatorChannel/Audio/Repository/CreatorChannelAudioRepository.swift new file mode 100644 index 00000000..9d3a642d --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Audio/Repository/CreatorChannelAudioRepository.swift @@ -0,0 +1,27 @@ +import Foundation +import Combine + +import CombineMoya +import Moya + +final class CreatorChannelAudioRepository { + private let api = MoyaProvider() + + func getAudio( + creatorId: Int, + page: Int, + size: Int, + sort: ContentSort, + themeId: Int? + ) -> AnyPublisher { + return api.requestPublisher( + .getAudio( + creatorId: creatorId, + page: page, + size: size, + sort: sort, + themeId: themeId + ) + ) + } +} diff --git a/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift b/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift index f5711eca..4aa46334 100644 --- a/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift +++ b/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift @@ -104,6 +104,15 @@ struct CreatorChannelView: View { .ignoresSafeArea(.container, edges: .bottom) } + if isOwnCreatorChannel && viewModel.selectedTab == .audio && !viewModel.isLoading { + VStack { + Spacer() + + CreatorChannelAudioUploadButton(action: showAudioContentUpload) + } + .ignoresSafeArea(.container, edges: .bottom) + } + if isShowAuthConfirmView { authConfirmDialog } @@ -207,6 +216,12 @@ struct CreatorChannelView: View { onTapLive: showLiveDetail, onTapContent: showContentDetail ) + } else if viewModel.selectedTab == .audio { + CreatorChannelAudioTabView( + creatorId: creatorId, + isOwnCreatorChannel: isOwnCreatorChannel, + onTapContent: showContentDetail + ) } else if viewModel.selectedTab != .home { CreatorChannelPlaceholderTabView(title: viewModel.selectedTab.title) } else if let response = viewModel.response { diff --git a/docs/20260704_크리에이터_채널_오디오_탭/plan-task.md b/docs/20260704_크리에이터_채널_오디오_탭/plan-task.md new file mode 100644 index 00000000..7f904ad4 --- /dev/null +++ b/docs/20260704_크리에이터_채널_오디오_탭/plan-task.md @@ -0,0 +1,143 @@ +# 크리에이터 채널 오디오 탭 구현 계획 + +## 기준 문서 + +- PRD: `docs/20260704_크리에이터_채널_오디오_탭/prd.md` +- 기존 라이브 탭 PRD: `docs/20260703_크리에이터_채널_라이브_탭/prd.md` +- 기존 라이브 탭 구현 계획: `docs/20260703_크리에이터_채널_라이브_탭/plan-task.md` +- 코드 스타일: `docs/agent-guides/code-style.md` +- 빌드/검증: `docs/agent-guides/build-test-verification.md` +- Figma 전체 화면: `290:9015`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=290-9015&m=dev` +- Figma 소장률: `290:9029`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=290-9029&m=dev` +- Figma Empty: `290:8965`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=290-8965&m=dev` +- Figma 본인 채널 CTA: `665:19008`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=665-19008&m=dev` + +## 구현 원칙 + +- 기존 `CreatorChannelView`의 공통 shell, header, sticky tab-bar, title bar 동작은 유지한다. +- `CreatorChannelTab.audio` 콘텐츠만 신규 `CreatorChannelAudioTabView`로 교체한다. +- `ContentSort`, `CreatorChannelSortBar`, `CreatorChannelSortBottomSheet`, `CapsuleTabBar`, `CreatorChannelAudioContentListItem`을 재사용한다. +- 전체 테마는 API 모델에 섞지 않고 오디오 탭 표시 모델에서만 합성한다. +- `themeId == nil`이면 query parameter를 보내지 않는다. +- `duration == nil` 또는 빈 문자열인 콘텐츠는 ViewModel에서 목록에 포함하지 않는다. +- 본인 채널 CTA는 기존 `showAudioContentUpload()` 흐름을 재사용한다. + +## 주요 대상 파일 + +### 생성 +- `SodaLive/Sources/V2/CreatorChannel/Audio/Models/CreatorChannelAudioTabResponse.swift` +- `SodaLive/Sources/V2/CreatorChannel/Audio/Repository/CreatorChannelAudioApi.swift` +- `SodaLive/Sources/V2/CreatorChannel/Audio/Repository/CreatorChannelAudioRepository.swift` +- `SodaLive/Sources/V2/CreatorChannel/Audio/CreatorChannelAudioViewModel.swift` +- `SodaLive/Sources/V2/CreatorChannel/Audio/CreatorChannelAudioTabView.swift` +- `SodaLive/Sources/V2/CreatorChannel/Audio/Components/CreatorChannelAudioOwnershipRateView.swift` +- `SodaLive/Sources/V2/CreatorChannel/Audio/Components/CreatorChannelAudioUploadButton.swift` + +### 수정 +- `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift` +- `SodaLive/Sources/I18n/I18n.swift` +- 필요 시 `SodaLive.xcodeproj/project.pbxproj` + +## TASK 체크리스트 + +### Phase 1: 문서와 API 모델 + +- [x] **Task 1.1: PRD와 계획 문서 생성** + - 대상 파일: + - 생성: `docs/20260704_크리에이터_채널_오디오_탭/prd.md` + - 생성: `docs/20260704_크리에이터_채널_오디오_탭/plan-task.md` + - 검증 기준: + - 실행 명령: `rg "290:9015|290:9029|290:8965|665:19008|CreatorChannelAudioTabResponse" docs/20260704_크리에이터_채널_오디오_탭` + - 기대 결과: Figma URL과 응답 모델 요구사항이 문서에 기록된다. + +- [x] **Task 1.2: 오디오 탭 Response model 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Audio/Models/CreatorChannelAudioTabResponse.swift` + - 작업 내용: + - `CreatorChannelAudioTabResponse`와 `CreatorChannelAudioThemeResponse`를 `Decodable`로 생성한다. + - `CreatorChannelAudioTabResponse.audioContents`는 기존 `[CreatorChannelAudioContentResponse]`를 사용한다. + - 검증 기준: + - 실행 명령: `rg "struct CreatorChannelAudioTabResponse|audioContentCount|paidAudioContentCount|purchasedAudioContentCount|purchasedAudioContentRate|themes|audioContents|themeId: Int\\?|struct CreatorChannelAudioThemeResponse" SodaLive/Sources/V2/CreatorChannel/Audio/Models/CreatorChannelAudioTabResponse.swift` + +### Phase 2: API, Repository, ViewModel + +- [x] **Task 2.1: 오디오 탭 API endpoint 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Audio/Repository/CreatorChannelAudioApi.swift` + - 작업 내용: + - `CreatorChannelAudioApi.getAudio(creatorId:page:size:sort:themeId:)`를 만든다. + - path는 `/api/v2/creator-channels/{creatorId}/audio`로 구성한다. + - `themeId`는 nil이 아닐 때만 query에 포함한다. + - 검증 기준: + - 실행 명령: `rg "CreatorChannelAudioApi|getAudio|/api/v2/creator-channels/|/audio|themeId|URLEncoding.queryString" SodaLive/Sources/V2/CreatorChannel/Audio/Repository/CreatorChannelAudioApi.swift` + +- [x] **Task 2.2: 오디오 탭 Repository 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Audio/Repository/CreatorChannelAudioRepository.swift` + - 작업 내용: + - `MoyaProvider`를 사용하는 `getAudio(...)` publisher를 제공한다. + - 검증 기준: + - 실행 명령: `rg "CreatorChannelAudioRepository|MoyaProvider|AnyPublisher|getAudio" SodaLive/Sources/V2/CreatorChannel/Audio/Repository/CreatorChannelAudioRepository.swift` + +- [x] **Task 2.3: 오디오 탭 ViewModel 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Audio/CreatorChannelAudioViewModel.swift` + - 작업 내용: + - 상태는 loading, response, filtered `audioContents`, selectedSort, selectedTheme, page, size, hasNext, hasLoaded, error toast를 둔다. + - `displayThemes`에서 전체 항목을 맨 앞에 합성한다. + - 정렬/테마 변경 시 첫 페이지부터 다시 조회한다. + - `duration`이 없는 콘텐츠는 `visibleAudioContents`에 포함하지 않는다. + - 검증 기준: + - 실행 명령: `rg "CreatorChannelAudioViewModel|displayThemes|selectedTheme|fetchFirstPage|fetchNextPageIfNeeded|selectSort|selectTheme|compactMap|duration" SodaLive/Sources/V2/CreatorChannel/Audio/CreatorChannelAudioViewModel.swift` + +### Phase 3: Figma 섹션별 UI + +- [x] **Task 3.1: Theme CapsuleTabBar 적용** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Audio/CreatorChannelAudioTabView.swift` + - 검증 기준: + - 실행 명령: `rg "CapsuleTabBar|displayThemes|selectedTheme" SodaLive/Sources/V2/CreatorChannel/Audio/CreatorChannelAudioTabView.swift` + +- [x] **Task 3.2: 소장률 UI 구현** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Audio/Components/CreatorChannelAudioOwnershipRateView.swift` + - 검증 기준: + - 실행 명령: `rg "CreatorChannelAudioOwnershipRateView|purchasedAudioContentRate|purchasedAudioContentCount|paidAudioContentCount|ProgressView|progress" SodaLive/Sources/V2/CreatorChannel/Audio/Components/CreatorChannelAudioOwnershipRateView.swift` + +- [x] **Task 3.3: 콘텐츠 목록과 empty 구현** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/CreatorChannel/Audio/CreatorChannelAudioTabView.swift` + - 수정: `SodaLive/Sources/I18n/I18n.swift` + - 검증 기준: + - 실행 명령: `rg "CreatorChannelAudioContentListItem|audioEmptyMessage|fetchNextPageIfNeeded|isEmptyState" SodaLive/Sources/V2/CreatorChannel/Audio/CreatorChannelAudioTabView.swift SodaLive/Sources/I18n/I18n.swift` + +- [x] **Task 3.4: 본인 채널 CTA 구현** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Audio/Components/CreatorChannelAudioUploadButton.swift` + - 검증 기준: + - 실행 명령: `rg "CreatorChannelAudioUploadButton|ic_new_upload_audio|uploadAudioContent|Capsule|frame\\(maxWidth: \\.infinity" SodaLive/Sources/V2/CreatorChannel/Audio/Components/CreatorChannelAudioUploadButton.swift` + +### Phase 4: CreatorChannelView 연결과 검증 + +- [x] **Task 4.1: 오디오 탭 콘텐츠와 CTA 연결** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift` + - 검증 기준: + - 실행 명령: `rg "CreatorChannelAudioTabView|viewModel\\.selectedTab == \\.audio|CreatorChannelAudioUploadButton|showAudioContentUpload" SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift` + +- [ ] **Task 4.2: 프로젝트 파일 등록과 정적 검증** + - 대상 파일: + - 확인/수정: `SodaLive.xcodeproj/project.pbxproj` + - 검증 기준: + - 실행 명령: `git diff --check -- docs/20260704_크리에이터_채널_오디오_탭 SodaLive/Sources/V2/CreatorChannel SodaLive/Sources/I18n/I18n.swift SodaLive.xcodeproj/project.pbxproj` + - 기대 결과: 공백 오류 없이 성공한다. + - 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` + - 기대 결과: 빌드 성공. + +## 검증 기록 + +- 2026-07-04: `get_design_context`와 `get_screenshot`으로 Figma `290:9015`, `290:9029`, `290:8965`, `665:19008`을 확인했다. +- 2026-07-04: `rg`와 `sed`로 기존 라이브 탭, 정렬 컴포넌트, `CapsuleTabBar`, 오디오 콘텐츠 item, 본인 채널 CTA 흐름을 확인했다. +- 2026-07-04: `git diff --check -- docs/20260704_크리에이터_채널_오디오_탭 SodaLive/Sources/V2/CreatorChannel SodaLive/Sources/I18n/I18n.swift SodaLive.xcodeproj/project.pbxproj` 실행 결과 출력 없이 성공했다. +- 2026-07-04: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`는 `SodaLive.xcworkspace`를 workspace로 인식하지 못해 실패했다. +- 2026-07-04: 대체로 `xcodebuild -project "SodaLive.xcodeproj" -scheme "SodaLive-dev" -configuration Debug build`를 샌드박스 밖에서 실행했다. SwiftPM 패키지 해석과 외부 패키지 컴파일은 진행됐으나 기존 외부 모듈 `Bootpay`, `BootpayUI`, `AgoraRtmKit`, `GoogleSignIn`, `YandexMobileAds`를 찾지 못해 앱 타깃 SwiftDriver 단계에서 실패했다. diff --git a/docs/20260704_크리에이터_채널_오디오_탭/prd.md b/docs/20260704_크리에이터_채널_오디오_탭/prd.md new file mode 100644 index 00000000..7f860576 --- /dev/null +++ b/docs/20260704_크리에이터_채널_오디오_탭/prd.md @@ -0,0 +1,121 @@ +# PRD: 크리에이터 채널 오디오 탭 + +## 1. Overview +크리에이터 채널 공통 shell의 `오디오` 탭에서 크리에이터의 오디오 콘텐츠 목록, 테마 필터, 정렬, 소장률 정보를 제공한다. 상단 title bar, header, sticky tab-bar는 기존 크리에이터 채널 홈 구현을 재사용하고, tab-bar 아래 콘텐츠만 오디오 탭 전용 API 응답으로 구성한다. + +API는 `GET /api/v2/creator-channels/{creatorId}/audio`를 사용한다. 기본 query는 `page=0`, `size=20`, `sort=LATEST`이며 `themeId`는 전체 선택 상태에서 보내지 않는다. 서버 응답의 `themes`에는 전체 항목이 포함되지 않으므로 클라이언트에서 맨 앞에 전체 탭을 추가한다. + +Figma 참조: +- 전체 화면: `290:9015`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=290-9015&m=dev` +- 소장률: `290:9029`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=290-9029&m=dev` +- Empty: `290:8965`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=290-8965&m=dev` +- 본인 채널 CTA: `665:19008`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=665-19008&m=dev` + +## 2. Problem +- 현재 `CreatorChannelTab.audio`는 placeholder로 표시되어 실제 오디오 목록, 테마 필터, 소장률, 페이지네이션이 없다. +- 오디오 탭은 라이브 탭과 같은 정렬 선택 UX를 사용해야 하지만, 테마 필터와 소장률 표시 조건이 추가된다. +- 본인 채널에서는 일반 사용자용 소장률 대신 오디오 업로드 CTA를 제공해야 한다. + +## 3. Goals +- `CreatorChannelTab.audio` 선택 시 오디오 탭 API를 호출하고 응답 데이터로 화면을 구성한다. +- `page`, `size`, `sort`, 선택된 `themeId`를 query parameter로 전달한다. +- 기본값은 `page=0`, `size=20`, `sort=LATEST`, `themeId=nil`이다. +- theme list는 `CapsuleTabBar`를 사용하고, 맨 앞에 전체 항목을 합성한다. +- 정렬 선택 방식은 라이브 탭과 동일한 `CreatorChannelSortBar` + `CreatorChannelSortBottomSheet`를 재사용한다. +- 콘텐츠 item은 `CreatorChannelAudioContentListItem`을 재사용한다. +- 전체 테마이고 본인 채널이 아닐 때만 소장률 UI를 표시한다. +- 본인 채널의 오디오 탭에는 `ic_new_upload_audio` 아이콘을 쓰는 하단 CTA를 표시한다. + +## 4. Non-Goals +- 크리에이터 채널 공통 shell, header, sticky tab-bar 동작을 다시 설계하지 않는다. +- 오디오 콘텐츠 상세, 구매, 재생, 업로드 화면 자체를 새로 구현하지 않는다. +- 신규 정렬 enum을 추가하지 않고 기존 `ContentSort`를 재사용한다. +- `CreatorChannelAudioContentListItem`의 표시 규칙을 이번 범위에서 재설계하지 않는다. +- Figma localhost asset URL을 앱 코드에 직접 사용하지 않는다. +- `Pods/**`, `generated/**`, `build/**`는 수정하지 않는다. + +## 5. Core Requirements + +### 5.1 API +- Method: `GET` +- Path: `/api/v2/creator-channels/{creatorId}/audio` +- Path parameter: `creatorId` +- Query parameters: + - `sort` + - `page` + - `size` + - `themeId` +- `themeId == nil`이면 query parameter에서 제외한다. +- 응답 래퍼는 기존 패턴대로 `ApiResponse`로 디코딩한다. + +```kotlin +data class CreatorChannelAudioTabResponse( + val audioContentCount: Int, + val paidAudioContentCount: Int, + val purchasedAudioContentCount: Int, + val purchasedAudioContentRate: Double, + val themes: List, + val audioContents: List, + val sort: ContentSort, + val themeId: Long?, + val page: Int, + val size: Int, + val hasNext: Boolean +) + +data class CreatorChannelAudioThemeResponse( + val themeId: Long, + val themeName: String +) +``` + +- `ContentSort`와 `CreatorChannelAudioContentResponse`는 기존 타입을 재사용한다. +- Kotlin `Long`은 기존 V2 모델 관례대로 Swift `Int`로 선언한다. + +### 5.2 Theme filter +- 서버 응답 `themes` 앞에 클라이언트 전용 전체 항목을 추가한다. +- 서버 응답 `themeId == nil`이면 전체가 선택된 상태여야 한다. +- 테마 선택 시 첫 페이지부터 다시 조회한다. +- 전체 선택 시 `themeId`를 보내지 않는다. + +### 5.3 Ownership rate +- 소장률 UI는 전체 테마이고 본인 채널이 아닐 때만 표시한다. +- 테마별 필터 상태에서는 표시하지 않는다. +- 본인 채널에서는 표시하지 않는다. +- `purchasedAudioContentRate`는 percent 값으로 내려오므로 그대로 `%` 문구에 사용한다. +- 우측 카운트는 `purchasedAudioContentCount/paidAudioContentCount개` 형식으로 표시한다. +- progress 값은 `purchasedAudioContentRate / 100`을 0...1로 clamp해서 사용한다. + +### 5.4 Audio contents +- `audioContents` 중 `duration == nil` 또는 빈 문자열인 콘텐츠는 숨긴다. +- Empty 상태 판정은 서버의 `audioContentCount == 0` 기준으로 한다. +- Empty 문구는 `크리에이터가 오디오를 준비 중입니다.\n기대해 주세요!`를 표시하고, UI 표시 방식은 라이브 탭 empty와 동일하게 한다. +- 목록 하단 도달 시 `hasNext == true`이면 `page + 1`을 조회해 append한다. + +### 5.5 Own channel CTA +- 본인 채널이면 소장률 UI를 숨긴다. +- 본인 채널이고 `selectedTab == .audio`일 때 하단 고정 CTA를 표시한다. +- CTA 표시 방식은 라이브 탭의 `라이브 시작하기` CTA와 동일한 full-width capsule 버튼이다. +- CTA icon은 `ic_new_upload_audio`를 사용한다. +- CTA tap은 기존 `showAudioContentUpload()` 흐름을 재사용한다. + +## 6. Success Criteria +- 오디오 탭 진입 시 `GET /api/v2/creator-channels/{creatorId}/audio?page=0&size=20&sort=LATEST`가 호출되고, 전체 선택 상태에서는 `themeId`가 전송되지 않는다. +- `themes` 앞에 전체 탭이 표시되고, 응답 `themeId == nil`이면 전체가 선택된다. +- 테마 선택 시 선택한 `themeId`로 첫 페이지를 다시 조회한다. +- 정렬 선택 UI와 동작은 라이브 탭과 동일하다. +- 본인 채널이 아니고 전체 테마일 때만 소장률 UI가 표시된다. +- 본인 채널이거나 테마 필터 상태이면 소장률 UI가 표시되지 않는다. +- `duration`이 없는 콘텐츠는 목록에 표시되지 않는다. +- `audioContentCount == 0`이면 라이브 탭과 같은 방식으로 오디오 empty 문구가 표시된다. +- 본인 채널의 오디오 탭 하단에는 `ic_new_upload_audio` 아이콘을 쓰는 CTA가 표시되고 오디오 콘텐츠 업로드로 이동한다. + +## 7. Technical Constraints +- 기능 변경은 `SodaLive/Sources/V2/CreatorChannel/**` 하위에서 해결한다. +- 오디오 탭 전용 View, ViewModel, Repository, API, 모델은 `SodaLive/Sources/V2/CreatorChannel/Audio/**` 아래에 둔다. +- 공용 item, sort bar, sort bottom sheet, `CapsuleTabBar`는 기존 구현을 재사용한다. +- 신규 문구는 `SodaLive/Sources/I18n/I18n.swift`에 ko/en/ja를 추가한다. + +## 8. Verification Notes +- 2026-07-04: Figma `get_design_context`와 `get_screenshot`으로 `290:9015`, `290:9029`, `290:8965`, `665:19008`을 확인했다. +- 2026-07-04: `CreatorChannelLiveTabView`, `CreatorChannelLiveViewModel`, `CreatorChannelSortBar`, `CapsuleTabBar`, `CreatorChannelAudioContentListItem` 기존 구현을 확인해 재사용 경계를 확정했다.