feat(creator): 오디오 탭을 추가한다

This commit is contained in:
Yu Sung
2026-07-04 16:40:35 +09:00
parent 6600cd43c1
commit c4c18341a6
12 changed files with 955 additions and 77 deletions

View File

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

View File

@@ -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: "最新オーディオ") }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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 단계에서 실패했다.

View 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` 기존 구현을 확인해 재사용 경계를 확정했다.