feat(creator): 오디오 탭을 추가한다
This commit is contained in:
@@ -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 = "<group>"; };
|
||||
CC0200022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveStartButton.swift; sourceTree = "<group>"; };
|
||||
CC0200032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveTabView.swift; sourceTree = "<group>"; };
|
||||
CC01002A2FF0100000A5BBA3 /* ContentSort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSort.swift; sourceTree = "<group>"; };
|
||||
CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelSortBar.swift; sourceTree = "<group>"; };
|
||||
CC01002C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelSortBottomSheet.swift; sourceTree = "<group>"; };
|
||||
CC01002D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveTabResponse.swift; sourceTree = "<group>"; };
|
||||
CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveApi.swift; sourceTree = "<group>"; };
|
||||
CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveRepository.swift; sourceTree = "<group>"; };
|
||||
CC0100302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveViewModel.swift; sourceTree = "<group>"; };
|
||||
0BBDC3ECBBE049EAA4057D3A /* PostChannelDonationRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PostChannelDonationRequest.swift; sourceTree = "<group>"; };
|
||||
0EC05859619BDC03056911E7 /* MainTab.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainTab.swift; sourceTree = "<group>"; };
|
||||
189350EA101CF12EEBA6EC1D /* MainTabBarView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainTabBarView.swift; sourceTree = "<group>"; };
|
||||
@@ -2657,6 +2668,23 @@
|
||||
CC0100272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelSnsSection.swift; sourceTree = "<group>"; };
|
||||
CC0100282FF0100000A5BBA3 /* CreatorChannelTalkActionSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelTalkActionSection.swift; sourceTree = "<group>"; };
|
||||
CC0100292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelFloatingActionMenu.swift; sourceTree = "<group>"; };
|
||||
CC01002A2FF0100000A5BBA3 /* ContentSort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSort.swift; sourceTree = "<group>"; };
|
||||
CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelSortBar.swift; sourceTree = "<group>"; };
|
||||
CC01002C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelSortBottomSheet.swift; sourceTree = "<group>"; };
|
||||
CC01002D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveTabResponse.swift; sourceTree = "<group>"; };
|
||||
CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveApi.swift; sourceTree = "<group>"; };
|
||||
CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveRepository.swift; sourceTree = "<group>"; };
|
||||
CC0100302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveViewModel.swift; sourceTree = "<group>"; };
|
||||
CC0200012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelAudioContentListItem.swift; sourceTree = "<group>"; };
|
||||
CC0200022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveStartButton.swift; sourceTree = "<group>"; };
|
||||
CC0200032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveTabView.swift; sourceTree = "<group>"; };
|
||||
CC0300012FF0300000A5BBA3 /* CreatorChannelAudioTabResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelAudioTabResponse.swift; sourceTree = "<group>"; };
|
||||
CC0300022FF0300000A5BBA3 /* CreatorChannelAudioApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelAudioApi.swift; sourceTree = "<group>"; };
|
||||
CC0300032FF0300000A5BBA3 /* CreatorChannelAudioRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelAudioRepository.swift; sourceTree = "<group>"; };
|
||||
CC0300042FF0300000A5BBA3 /* CreatorChannelAudioViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelAudioViewModel.swift; sourceTree = "<group>"; };
|
||||
CC0300052FF0300000A5BBA3 /* CreatorChannelAudioOwnershipRateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelAudioOwnershipRateView.swift; sourceTree = "<group>"; };
|
||||
CC0300062FF0300000A5BBA3 /* CreatorChannelAudioUploadButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelAudioUploadButton.swift; sourceTree = "<group>"; };
|
||||
CC0300072FF0300000A5BBA3 /* CreatorChannelAudioTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelAudioTabView.swift; sourceTree = "<group>"; };
|
||||
CC73FFC54CB1D988053059D1 /* LanguageEnvironment.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LanguageEnvironment.swift; sourceTree = "<group>"; };
|
||||
CE82912F2F111593BE42E33C /* CommunityPostCard.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommunityPostCard.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
@@ -2744,43 +2772,6 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
CC0200042FF0200000A5BBA3 /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CC0200022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CC0100312FF0100000A5BBA3 /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CC01002D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CC0100322FF0100000A5BBA3 /* Repository */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */,
|
||||
CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */,
|
||||
);
|
||||
path = Repository;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CC0100332FF0100000A5BBA3 /* Live */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CC0100312FF0100000A5BBA3 /* Models */,
|
||||
CC0100322FF0100000A5BBA3 /* Repository */,
|
||||
CC0200042FF0200000A5BBA3 /* Components */,
|
||||
CC0100302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift */,
|
||||
CC0200032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift */,
|
||||
);
|
||||
path = Live;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
CC0100312FF0100000A5BBA3 /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CC01002D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CC0100322FF0100000A5BBA3 /* Repository */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */,
|
||||
CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */,
|
||||
);
|
||||
path = Repository;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CC0100332FF0100000A5BBA3 /* Live */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CC0100312FF0100000A5BBA3 /* Models */,
|
||||
CC0100322FF0100000A5BBA3 /* Repository */,
|
||||
CC0200042FF0200000A5BBA3 /* Components */,
|
||||
CC0100302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift */,
|
||||
CC0200032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift */,
|
||||
);
|
||||
path = Live;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CC0200042FF0200000A5BBA3 /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CC0200022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CC0300082FF0300000A5BBA3 /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CC0300012FF0300000A5BBA3 /* CreatorChannelAudioTabResponse.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CC0300092FF0300000A5BBA3 /* Repository */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CC0300022FF0300000A5BBA3 /* CreatorChannelAudioApi.swift */,
|
||||
CC0300032FF0300000A5BBA3 /* CreatorChannelAudioRepository.swift */,
|
||||
);
|
||||
path = Repository;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CC0300102FF0300000A5BBA3 /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CC0300052FF0300000A5BBA3 /* CreatorChannelAudioOwnershipRateView.swift */,
|
||||
CC0300062FF0300000A5BBA3 /* CreatorChannelAudioUploadButton.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CC0300112FF0300000A5BBA3 /* Audio */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CC0300082FF0300000A5BBA3 /* Models */,
|
||||
CC0300092FF0300000A5BBA3 /* Repository */,
|
||||
CC0300102FF0300000A5BBA3 /* Components */,
|
||||
CC0300042FF0300000A5BBA3 /* CreatorChannelAudioViewModel.swift */,
|
||||
CC0300072FF0300000A5BBA3 /* CreatorChannelAudioTabView.swift */,
|
||||
);
|
||||
path = Audio;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
||||
@@ -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: "最新オーディオ") }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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<AnyCancellable>()
|
||||
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<CreatorChannelAudioTabResponse>.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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
import CombineMoya
|
||||
import Moya
|
||||
|
||||
final class CreatorChannelAudioRepository {
|
||||
private let api = MoyaProvider<CreatorChannelAudioApi>()
|
||||
|
||||
func getAudio(
|
||||
creatorId: Int,
|
||||
page: Int,
|
||||
size: Int,
|
||||
sort: ContentSort,
|
||||
themeId: Int?
|
||||
) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getAudio(
|
||||
creatorId: creatorId,
|
||||
page: page,
|
||||
size: size,
|
||||
sort: sort,
|
||||
themeId: themeId
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
143
docs/20260704_크리에이터_채널_오디오_탭/plan-task.md
Normal file
143
docs/20260704_크리에이터_채널_오디오_탭/plan-task.md
Normal file
@@ -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<CreatorChannelAudioApi>`를 사용하는 `getAudio(...)` publisher를 제공한다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "CreatorChannelAudioRepository|MoyaProvider<CreatorChannelAudioApi>|AnyPublisher<Response, MoyaError>|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 단계에서 실패했다.
|
||||
121
docs/20260704_크리에이터_채널_오디오_탭/prd.md
Normal file
121
docs/20260704_크리에이터_채널_오디오_탭/prd.md
Normal file
@@ -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<CreatorChannelAudioTabResponse>`로 디코딩한다.
|
||||
|
||||
```kotlin
|
||||
data class CreatorChannelAudioTabResponse(
|
||||
val audioContentCount: Int,
|
||||
val paidAudioContentCount: Int,
|
||||
val purchasedAudioContentCount: Int,
|
||||
val purchasedAudioContentRate: Double,
|
||||
val themes: List<CreatorChannelAudioThemeResponse>,
|
||||
val audioContents: List<CreatorChannelAudioContentResponse>,
|
||||
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` 기존 구현을 확인해 재사용 경계를 확정했다.
|
||||
Reference in New Issue
Block a user