feat(creator): 팬Talk 탭을 추가한다
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
00C796627A6758D271215EC5 /* CreatorProfileItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26881ED30292E28D2E5F84CD /* CreatorProfileItem.swift */; };
|
00C796627A6758D271215EC5 /* CreatorProfileItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26881ED30292E28D2E5F84CD /* CreatorProfileItem.swift */; };
|
||||||
042BEE0C973700FB5F4540CD /* I18n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826D626B7714E5C28ED31074 /* I18n.swift */; };
|
042BEE0C973700FB5F4540CD /* I18n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826D626B7714E5C28ED31074 /* I18n.swift */; };
|
||||||
0470E68C6F302A131264E3AF /* ChannelDonationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF2BFD2E09F509C44C92214 /* ChannelDonationViewModel.swift */; };
|
0470E68C6F302A131264E3AF /* ChannelDonationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF2BFD2E09F509C44C92214 /* ChannelDonationViewModel.swift */; };
|
||||||
|
07F442FCA64E7F5D72E134CC /* CreatorChannelFanTalkApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 002435A7145D59CCCA8F76D1 /* CreatorChannelFanTalkApi.swift */; };
|
||||||
109DD8670CD0F25C0F436455 /* V2vRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = E56C8151F551B091940F16C4 /* V2vRepository.swift */; };
|
109DD8670CD0F25C0F436455 /* V2vRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = E56C8151F551B091940F16C4 /* V2vRepository.swift */; };
|
||||||
116B445BDD4D507AF3FBD2B1 /* LanguageRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1165E3F4CA1AB019FC85067 /* LanguageRepository.swift */; };
|
116B445BDD4D507AF3FBD2B1 /* LanguageRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1165E3F4CA1AB019FC85067 /* LanguageRepository.swift */; };
|
||||||
14A88B232976EAD54B876195 /* LanguageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB63E7E31B75FF2E6F1C9073 /* LanguageSettingsView.swift */; };
|
14A88B232976EAD54B876195 /* LanguageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB63E7E31B75FF2E6F1C9073 /* LanguageSettingsView.swift */; };
|
||||||
@@ -19,16 +20,21 @@
|
|||||||
176AE05194A71107EA461B7B /* AiCharacterCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A575789542808C0C87AC6D82 /* AiCharacterCard.swift */; };
|
176AE05194A71107EA461B7B /* AiCharacterCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A575789542808C0C87AC6D82 /* AiCharacterCard.swift */; };
|
||||||
1DD00C45C183A84D0CD5553F /* CreatorProfileItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26881ED30292E28D2E5F84CD /* CreatorProfileItem.swift */; };
|
1DD00C45C183A84D0CD5553F /* CreatorProfileItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26881ED30292E28D2E5F84CD /* CreatorProfileItem.swift */; };
|
||||||
1F1646CFB87FE6E699766893 /* CreatorProfileGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6966E927CDB1DE31F936757 /* CreatorProfileGrid.swift */; };
|
1F1646CFB87FE6E699766893 /* CreatorProfileGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6966E927CDB1DE31F936757 /* CreatorProfileGrid.swift */; };
|
||||||
|
1FA2B186FCA7B8B83648CF77 /* CreatorChannelFanTalkEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A08977117E94D72914BB3FA /* CreatorChannelFanTalkEmptyView.swift */; };
|
||||||
22DCFBA6342619CDC5785801 /* PushNotificationListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C21D69F717F8D80644F6AEE /* PushNotificationListView.swift */; };
|
22DCFBA6342619CDC5785801 /* PushNotificationListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C21D69F717F8D80644F6AEE /* PushNotificationListView.swift */; };
|
||||||
2A49520637FD59B21473C7C2 /* PostChannelDonationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BBDC3ECBBE049EAA4057D3A /* PostChannelDonationRequest.swift */; };
|
2A49520637FD59B21473C7C2 /* PostChannelDonationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BBDC3ECBBE049EAA4057D3A /* PostChannelDonationRequest.swift */; };
|
||||||
2B35B93B2F7253CADBC12A82 /* DateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375C41DF9D62AF5B9C3C1FAC /* DateParser.swift */; };
|
2B35B93B2F7253CADBC12A82 /* DateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375C41DF9D62AF5B9C3C1FAC /* DateParser.swift */; };
|
||||||
2B65EEB59EE24059AC5DEADC /* BannerCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24266E32FC49D7C31610C2AD /* BannerCarousel.swift */; };
|
2B65EEB59EE24059AC5DEADC /* BannerCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24266E32FC49D7C31610C2AD /* BannerCarousel.swift */; };
|
||||||
|
2BA6CE345486A0F75F7E75DA /* CreatorChannelFanTalkActionPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D241AC104728809A8E70C8 /* CreatorChannelFanTalkActionPopup.swift */; };
|
||||||
|
2D761BB45AB6A332BE4690A1 /* CreatorChannelFanTalkEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A08977117E94D72914BB3FA /* CreatorChannelFanTalkEmptyView.swift */; };
|
||||||
|
2DA2EA227CAA0E757E63A335 /* CreatorChannelFanTalkCountBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDB71B1A61924B10599005FE /* CreatorChannelFanTalkCountBar.swift */; };
|
||||||
2FEE936CFEFBB634CAC7FE71 /* LanguageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7132AA3D131A8DF7B8D783BE /* LanguageService.swift */; };
|
2FEE936CFEFBB634CAC7FE71 /* LanguageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7132AA3D131A8DF7B8D783BE /* LanguageService.swift */; };
|
||||||
2FF0C36B82F67B9CC05B6942 /* LanguageOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7C4F8D708958D3E58AB9A77 /* LanguageOption.swift */; };
|
2FF0C36B82F67B9CC05B6942 /* LanguageOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7C4F8D708958D3E58AB9A77 /* LanguageOption.swift */; };
|
||||||
32595EDE75871A7B2FDCC5AB /* SectionTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16C12DFB83FE164B38CC066 /* SectionTitle.swift */; };
|
32595EDE75871A7B2FDCC5AB /* SectionTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16C12DFB83FE164B38CC066 /* SectionTitle.swift */; };
|
||||||
33BA3AB731849284E62AE04B /* MainTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC05859619BDC03056911E7 /* MainTab.swift */; };
|
33BA3AB731849284E62AE04B /* MainTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC05859619BDC03056911E7 /* MainTab.swift */; };
|
||||||
372837FE9857C16B6B5087FF /* TitleBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07CBA176DB8BFF3F8D6BCEE /* TitleBar.swift */; };
|
372837FE9857C16B6B5087FF /* TitleBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07CBA176DB8BFF3F8D6BCEE /* TitleBar.swift */; };
|
||||||
38D73A7B6BFB9A984834984B /* TextTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E61608194B084224DB6B78B5 /* TextTabBar.swift */; };
|
38D73A7B6BFB9A984834984B /* TextTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E61608194B084224DB6B78B5 /* TextTabBar.swift */; };
|
||||||
|
3BEBC2613F5CCEC2118B497B /* CreatorChannelFanTalkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C5D06E4F54C0ED7B7BD462 /* CreatorChannelFanTalkViewModel.swift */; };
|
||||||
3F829933ED2755E9F0BBC5FE /* KakaoSDKAuth in Frameworks */ = {isa = PBXBuildFile; productRef = E9CD5F29965B148AB360C353 /* KakaoSDKAuth */; };
|
3F829933ED2755E9F0BBC5FE /* KakaoSDKAuth in Frameworks */ = {isa = PBXBuildFile; productRef = E9CD5F29965B148AB360C353 /* KakaoSDKAuth */; };
|
||||||
4004671F2CDE08E6004DB9B5 /* PurchaseOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4004671E2CDE08E6004DB9B5 /* PurchaseOption.swift */; };
|
4004671F2CDE08E6004DB9B5 /* PurchaseOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4004671E2CDE08E6004DB9B5 /* PurchaseOption.swift */; };
|
||||||
400467202CDE08E6004DB9B5 /* PurchaseOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4004671E2CDE08E6004DB9B5 /* PurchaseOption.swift */; };
|
400467202CDE08E6004DB9B5 /* PurchaseOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4004671E2CDE08E6004DB9B5 /* PurchaseOption.swift */; };
|
||||||
@@ -1594,25 +1600,32 @@
|
|||||||
40FFB28F2E78517F004A0323 /* OriginalWorkCharactersPageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40FFB28D2E78517F004A0323 /* OriginalWorkCharactersPageResponse.swift */; };
|
40FFB28F2E78517F004A0323 /* OriginalWorkCharactersPageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40FFB28D2E78517F004A0323 /* OriginalWorkCharactersPageResponse.swift */; };
|
||||||
417D6105D408DD68A463451E /* Radius.swift in Sources */ = {isa = PBXBuildFile; fileRef = E31B21FA615404F2E9E7839F /* Radius.swift */; };
|
417D6105D408DD68A463451E /* Radius.swift in Sources */ = {isa = PBXBuildFile; fileRef = E31B21FA615404F2E9E7839F /* Radius.swift */; };
|
||||||
4317A150B48E89578596A474 /* AcceptLanguageDebugLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C22ACAC456B3BCB2D9C568D /* AcceptLanguageDebugLogger.swift */; };
|
4317A150B48E89578596A474 /* AcceptLanguageDebugLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C22ACAC456B3BCB2D9C568D /* AcceptLanguageDebugLogger.swift */; };
|
||||||
|
4466976AB5338188E0B6D536 /* CreatorChannelFanTalkRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF47BCA4DC51200D63D1E2D7 /* CreatorChannelFanTalkRepository.swift */; };
|
||||||
453D9A3A23598E513120303F /* AcceptLanguageDebugLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C22ACAC456B3BCB2D9C568D /* AcceptLanguageDebugLogger.swift */; };
|
453D9A3A23598E513120303F /* AcceptLanguageDebugLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C22ACAC456B3BCB2D9C568D /* AcceptLanguageDebugLogger.swift */; };
|
||||||
45436503B5AA5530DBBD3481 /* KakaoSDKCommon in Frameworks */ = {isa = PBXBuildFile; productRef = 00EE224A913C7F8ADC48C15E /* KakaoSDKCommon */; };
|
45436503B5AA5530DBBD3481 /* KakaoSDKCommon in Frameworks */ = {isa = PBXBuildFile; productRef = 00EE224A913C7F8ADC48C15E /* KakaoSDKCommon */; };
|
||||||
4572B5004B70CED10BDDB4C3 /* FollowAllButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348933CFF7DC1B9DB6E5D1A1 /* FollowAllButton.swift */; };
|
4572B5004B70CED10BDDB4C3 /* FollowAllButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348933CFF7DC1B9DB6E5D1A1 /* FollowAllButton.swift */; };
|
||||||
4A22DB69D003B207E35CFF51 /* FontModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25D89184D67BD63E46E90281 /* FontModifier.swift */; };
|
4A22DB69D003B207E35CFF51 /* FontModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25D89184D67BD63E46E90281 /* FontModifier.swift */; };
|
||||||
4A951EA6E07524A0B650F4BF /* DateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375C41DF9D62AF5B9C3C1FAC /* DateParser.swift */; };
|
4A951EA6E07524A0B650F4BF /* DateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375C41DF9D62AF5B9C3C1FAC /* DateParser.swift */; };
|
||||||
4D1649A808884B53416FCC0E /* UpdateContentPreferenceResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4AAC943F77B6FE089696909 /* UpdateContentPreferenceResponse.swift */; };
|
4D1649A808884B53416FCC0E /* UpdateContentPreferenceResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4AAC943F77B6FE089696909 /* UpdateContentPreferenceResponse.swift */; };
|
||||||
|
4FDDF9E4DF4F003D30DDA8CE /* CreatorChannelFanTalkActionPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D241AC104728809A8E70C8 /* CreatorChannelFanTalkActionPopup.swift */; };
|
||||||
508015CDE6EB31076EBF695A /* CommunityPostCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE82912F2F111593BE42E33C /* CommunityPostCard.swift */; };
|
508015CDE6EB31076EBF695A /* CommunityPostCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE82912F2F111593BE42E33C /* CommunityPostCard.swift */; };
|
||||||
50958256B935286787BB9011 /* V2vModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4AF495E30F2DB00C2FCB36 /* V2vModels.swift */; };
|
50958256B935286787BB9011 /* V2vModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4AF495E30F2DB00C2FCB36 /* V2vModels.swift */; };
|
||||||
53E20A90F5D084820C7166E2 /* ChannelDonationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF2BFD2E09F509C44C92214 /* ChannelDonationViewModel.swift */; };
|
53E20A90F5D084820C7166E2 /* ChannelDonationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF2BFD2E09F509C44C92214 /* ChannelDonationViewModel.swift */; };
|
||||||
5469DF62DA655F2B8275034E /* LanguageEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC73FFC54CB1D988053059D1 /* LanguageEnvironment.swift */; };
|
5469DF62DA655F2B8275034E /* LanguageEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC73FFC54CB1D988053059D1 /* LanguageEnvironment.swift */; };
|
||||||
54B4C3DA47168500D42576F5 /* GetCreatorDetailResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5D253179845EF7577A1E01 /* GetCreatorDetailResponse.swift */; };
|
54B4C3DA47168500D42576F5 /* GetCreatorDetailResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5D253179845EF7577A1E01 /* GetCreatorDetailResponse.swift */; };
|
||||||
560889894E241FA549958716 /* AppDeepLinkHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CF63CA56D78A23F93C21E5 /* AppDeepLinkHandler.swift */; };
|
560889894E241FA549958716 /* AppDeepLinkHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CF63CA56D78A23F93C21E5 /* AppDeepLinkHandler.swift */; };
|
||||||
|
5AE12C82F7B160F075A5A6A8 /* CreatorChannelFanTalkTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F1C1B0C73E8D92112E47610 /* CreatorChannelFanTalkTabResponse.swift */; };
|
||||||
|
5B9BFA224A49A5AF0757FDF7 /* CreatorChannelFloatingIconButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40163E3E9DB325EA984D3C4 /* CreatorChannelFloatingIconButton.swift */; };
|
||||||
5BBF943332EF53658DD0A7D4 /* HomeTitleBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAA81E8424A575A0F8546D31 /* HomeTitleBar.swift */; };
|
5BBF943332EF53658DD0A7D4 /* HomeTitleBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAA81E8424A575A0F8546D31 /* HomeTitleBar.swift */; };
|
||||||
5EEE98FE0EC4039761C604EE /* TitleBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07CBA176DB8BFF3F8D6BCEE /* TitleBar.swift */; };
|
5EEE98FE0EC4039761C604EE /* TitleBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07CBA176DB8BFF3F8D6BCEE /* TitleBar.swift */; };
|
||||||
619BEFA3671DC15930F44957 /* PushNotificationListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BB17B6A2E9D9CAEA3B185D8 /* PushNotificationListViewModel.swift */; };
|
619BEFA3671DC15930F44957 /* PushNotificationListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BB17B6A2E9D9CAEA3B185D8 /* PushNotificationListViewModel.swift */; };
|
||||||
626310CE3DFB42589CDBE2F0 /* KakaoSDKCommon in Frameworks */ = {isa = PBXBuildFile; productRef = 6D2F147B06E56689E9EAFFDF /* KakaoSDKCommon */; };
|
626310CE3DFB42589CDBE2F0 /* KakaoSDKCommon in Frameworks */ = {isa = PBXBuildFile; productRef = 6D2F147B06E56689E9EAFFDF /* KakaoSDKCommon */; };
|
||||||
643F8CB19A85CA3C43475B7A /* LanguageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB63E7E31B75FF2E6F1C9073 /* LanguageSettingsView.swift */; };
|
643F8CB19A85CA3C43475B7A /* LanguageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB63E7E31B75FF2E6F1C9073 /* LanguageSettingsView.swift */; };
|
||||||
670CD825F9E01C6EFAC7BDCE /* LanguageOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7C4F8D708958D3E58AB9A77 /* LanguageOption.swift */; };
|
670CD825F9E01C6EFAC7BDCE /* LanguageOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7C4F8D708958D3E58AB9A77 /* LanguageOption.swift */; };
|
||||||
|
69DBCDB25941100E1CFF8C50 /* CreatorChannelFloatingIconButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40163E3E9DB325EA984D3C4 /* CreatorChannelFloatingIconButton.swift */; };
|
||||||
|
6A28B7059D970EC4E882EEA1 /* CreatorChannelFanTalkListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D354C827944EC15C7D0FFBE0 /* CreatorChannelFanTalkListItem.swift */; };
|
||||||
6B7A9ED264B5909F2E9F6E1C /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72CD656CAEE376701F2B563E /* MainView.swift */; };
|
6B7A9ED264B5909F2E9F6E1C /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72CD656CAEE376701F2B563E /* MainView.swift */; };
|
||||||
|
6CDDA1EEA4D2E47A563FD687 /* CreatorChannelFanTalkListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D354C827944EC15C7D0FFBE0 /* CreatorChannelFanTalkListItem.swift */; };
|
||||||
6FF4EB9BF0C754D1C31B347F /* V2vState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4339CA77E17ECD66D39D6379 /* V2vState.swift */; };
|
6FF4EB9BF0C754D1C31B347F /* V2vState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4339CA77E17ECD66D39D6379 /* V2vState.swift */; };
|
||||||
70E2CE7366BDBAC59BB39B41 /* AppDeepLinkHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CF63CA56D78A23F93C21E5 /* AppDeepLinkHandler.swift */; };
|
70E2CE7366BDBAC59BB39B41 /* AppDeepLinkHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CF63CA56D78A23F93C21E5 /* AppDeepLinkHandler.swift */; };
|
||||||
723D2E30F7D7B0F6711EA589 /* GetPushNotificationListResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F23D0834690CFE9FBDC10510 /* GetPushNotificationListResponse.swift */; };
|
723D2E30F7D7B0F6711EA589 /* GetPushNotificationListResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F23D0834690CFE9FBDC10510 /* GetPushNotificationListResponse.swift */; };
|
||||||
@@ -1639,6 +1652,7 @@
|
|||||||
8F73F4C392BA01ADCFDA907E /* CreatorProfileGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6966E927CDB1DE31F936757 /* CreatorProfileGrid.swift */; };
|
8F73F4C392BA01ADCFDA907E /* CreatorProfileGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6966E927CDB1DE31F936757 /* CreatorProfileGrid.swift */; };
|
||||||
8FD5E333B513198F5468A80C /* AcceptLanguagePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C076E80A2C92FEFFA5AB752 /* AcceptLanguagePlugin.swift */; };
|
8FD5E333B513198F5468A80C /* AcceptLanguagePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C076E80A2C92FEFFA5AB752 /* AcceptLanguagePlugin.swift */; };
|
||||||
9044732EEC6F1345875FAFFF /* UpdateContentPreferenceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDFAA64C2CA4FCBBF00A024 /* UpdateContentPreferenceRequest.swift */; };
|
9044732EEC6F1345875FAFFF /* UpdateContentPreferenceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDFAA64C2CA4FCBBF00A024 /* UpdateContentPreferenceRequest.swift */; };
|
||||||
|
905BA213B28A010FDE2D9E0C /* CreatorChannelFanTalkTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C7923ABAD25E9CC4840EC94 /* CreatorChannelFanTalkTabView.swift */; };
|
||||||
924411F1C774C065BAC45354 /* PushNotificationApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = F26CB443DA0ED4C45A18B258 /* PushNotificationApi.swift */; };
|
924411F1C774C065BAC45354 /* PushNotificationApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = F26CB443DA0ED4C45A18B258 /* PushNotificationApi.swift */; };
|
||||||
9346B50E2D4531CE2CE373C8 /* FollowAllButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348933CFF7DC1B9DB6E5D1A1 /* FollowAllButton.swift */; };
|
9346B50E2D4531CE2CE373C8 /* FollowAllButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348933CFF7DC1B9DB6E5D1A1 /* FollowAllButton.swift */; };
|
||||||
938F4F60253B2E551A7474B6 /* MainTabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 189350EA101CF12EEBA6EC1D /* MainTabBarView.swift */; };
|
938F4F60253B2E551A7474B6 /* MainTabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 189350EA101CF12EEBA6EC1D /* MainTabBarView.swift */; };
|
||||||
@@ -1660,6 +1674,7 @@
|
|||||||
ACD71056E97E13E48941ABBB /* MainPlaceholderTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7900EA9FD1150BABE64CF1B3 /* MainPlaceholderTabView.swift */; };
|
ACD71056E97E13E48941ABBB /* MainPlaceholderTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7900EA9FD1150BABE64CF1B3 /* MainPlaceholderTabView.swift */; };
|
||||||
AD6424211AA898794BBE8470 /* SectionTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16C12DFB83FE164B38CC066 /* SectionTitle.swift */; };
|
AD6424211AA898794BBE8470 /* SectionTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16C12DFB83FE164B38CC066 /* SectionTitle.swift */; };
|
||||||
AEFFD4DC842837ACF98BE697 /* PostChannelDonationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BBDC3ECBBE049EAA4057D3A /* PostChannelDonationRequest.swift */; };
|
AEFFD4DC842837ACF98BE697 /* PostChannelDonationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BBDC3ECBBE049EAA4057D3A /* PostChannelDonationRequest.swift */; };
|
||||||
|
B0DD3E23BFA368AD7007AF23 /* CreatorChannelFanTalkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C5D06E4F54C0ED7B7BD462 /* CreatorChannelFanTalkViewModel.swift */; };
|
||||||
B11681AC02BF51C6E2FA6FF3 /* MainTabBarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F49E8BB9BACFCA8878B70D /* MainTabBarButton.swift */; };
|
B11681AC02BF51C6E2FA6FF3 /* MainTabBarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F49E8BB9BACFCA8878B70D /* MainTabBarButton.swift */; };
|
||||||
B3AF650899B8FC84E0C0CDA0 /* MainPlaceholderTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7900EA9FD1150BABE64CF1B3 /* MainPlaceholderTabView.swift */; };
|
B3AF650899B8FC84E0C0CDA0 /* MainPlaceholderTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7900EA9FD1150BABE64CF1B3 /* MainPlaceholderTabView.swift */; };
|
||||||
B5E9FA82CF4238F640702D4A /* CreatorDetailDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7406AE69F7667FAA2E6DD23 /* CreatorDetailDialogView.swift */; };
|
B5E9FA82CF4238F640702D4A /* CreatorDetailDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7406AE69F7667FAA2E6DD23 /* CreatorDetailDialogView.swift */; };
|
||||||
@@ -1860,8 +1875,12 @@
|
|||||||
CC0530082FF0500000A5BBA3 /* CreatorChannelDonationEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0500082FF0500000A5BBA3 /* CreatorChannelDonationEmptyView.swift */; };
|
CC0530082FF0500000A5BBA3 /* CreatorChannelDonationEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0500082FF0500000A5BBA3 /* CreatorChannelDonationEmptyView.swift */; };
|
||||||
CC0530092FF0500000A5BBA3 /* CreatorChannelDonationFloatingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0500092FF0500000A5BBA3 /* CreatorChannelDonationFloatingButton.swift */; };
|
CC0530092FF0500000A5BBA3 /* CreatorChannelDonationFloatingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0500092FF0500000A5BBA3 /* CreatorChannelDonationFloatingButton.swift */; };
|
||||||
CC05300A2FF0500000A5BBA3 /* CreatorChannelDonationCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC05000A2FF0500000A5BBA3 /* CreatorChannelDonationCard.swift */; };
|
CC05300A2FF0500000A5BBA3 /* CreatorChannelDonationCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC05000A2FF0500000A5BBA3 /* CreatorChannelDonationCard.swift */; };
|
||||||
|
CD256952CC366FFB67557807 /* CreatorChannelFanTalkApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 002435A7145D59CCCA8F76D1 /* CreatorChannelFanTalkApi.swift */; };
|
||||||
|
CE935959F7A2BF7301F517DB /* CreatorChannelFanTalkTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F1C1B0C73E8D92112E47610 /* CreatorChannelFanTalkTabResponse.swift */; };
|
||||||
|
CFC28133D4D60FD1E436D97C /* CreatorChannelFanTalkTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C7923ABAD25E9CC4840EC94 /* CreatorChannelFanTalkTabView.swift */; };
|
||||||
D1C65856889B16DFDB69A004 /* PushNotificationRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDDF6BBD0192819B1F5B3FE /* PushNotificationRepository.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 */; };
|
D2A268C7346E84E6B455E25A /* Pods_SodaLive_dev.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F4E5BB7961B7E6BB68C3379 /* Pods_SodaLive_dev.framework */; };
|
||||||
|
DC3B6D07F71D7CFA83D91321 /* CreatorChannelFanTalkRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF47BCA4DC51200D63D1E2D7 /* CreatorChannelFanTalkRepository.swift */; };
|
||||||
DD202E48D61F1CE8108AD164 /* LineSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 6DBA0C325ACEC3FCDB22490D /* LineSDK */; };
|
DD202E48D61F1CE8108AD164 /* LineSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 6DBA0C325ACEC3FCDB22490D /* LineSDK */; };
|
||||||
DD2B6C23087BCA5591CD4D68 /* UpdateContentPreferenceResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4AAC943F77B6FE089696909 /* UpdateContentPreferenceResponse.swift */; };
|
DD2B6C23087BCA5591CD4D68 /* UpdateContentPreferenceResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4AAC943F77B6FE089696909 /* UpdateContentPreferenceResponse.swift */; };
|
||||||
E260227D30A9E44894BE3B27 /* V2vApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613FD451377C66254773CF47 /* V2vApi.swift */; };
|
E260227D30A9E44894BE3B27 /* V2vApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613FD451377C66254773CF47 /* V2vApi.swift */; };
|
||||||
@@ -1876,12 +1895,14 @@
|
|||||||
FA5F18B3EE4CBD02D3807BD1 /* MainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5420BBA14C00EC0D927BDAB /* MainViewModel.swift */; };
|
FA5F18B3EE4CBD02D3807BD1 /* MainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5420BBA14C00EC0D927BDAB /* MainViewModel.swift */; };
|
||||||
FB404343AB7668FB7D020C02 /* PushNotificationRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDDF6BBD0192819B1F5B3FE /* PushNotificationRepository.swift */; };
|
FB404343AB7668FB7D020C02 /* PushNotificationRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDDF6BBD0192819B1F5B3FE /* PushNotificationRepository.swift */; };
|
||||||
FCA61533BA73D91011BEC96F /* Spacing.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B18C00DE256DCC53030630 /* Spacing.swift */; };
|
FCA61533BA73D91011BEC96F /* Spacing.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B18C00DE256DCC53030630 /* Spacing.swift */; };
|
||||||
|
FD8075DBF9A40BA7544661FB /* CreatorChannelFanTalkCountBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDB71B1A61924B10599005FE /* CreatorChannelFanTalkCountBar.swift */; };
|
||||||
FE286EDC3151C5B7D62E5445 /* V2vApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613FD451377C66254773CF47 /* V2vApi.swift */; };
|
FE286EDC3151C5B7D62E5445 /* V2vApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613FD451377C66254773CF47 /* V2vApi.swift */; };
|
||||||
FE3244627FFAD8667541CE01 /* ChannelDonationAllView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 872F4F3506F36E0EAAE312ED /* ChannelDonationAllView.swift */; };
|
FE3244627FFAD8667541CE01 /* ChannelDonationAllView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 872F4F3506F36E0EAAE312ED /* ChannelDonationAllView.swift */; };
|
||||||
FE3C5D4BCD99FE5D5B389F03 /* GetCreatorDetailResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5D253179845EF7577A1E01 /* GetCreatorDetailResponse.swift */; };
|
FE3C5D4BCD99FE5D5B389F03 /* GetCreatorDetailResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5D253179845EF7577A1E01 /* GetCreatorDetailResponse.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
002435A7145D59CCCA8F76D1 /* CreatorChannelFanTalkApi.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreatorChannelFanTalkApi.swift; sourceTree = "<group>"; };
|
||||||
0BBDC3ECBBE049EAA4057D3A /* PostChannelDonationRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PostChannelDonationRequest.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>"; };
|
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>"; };
|
189350EA101CF12EEBA6EC1D /* MainTabBarView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainTabBarView.swift; sourceTree = "<group>"; };
|
||||||
@@ -1891,10 +1912,12 @@
|
|||||||
1FDDF6BBD0192819B1F5B3FE /* PushNotificationRepository.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PushNotificationRepository.swift; sourceTree = "<group>"; };
|
1FDDF6BBD0192819B1F5B3FE /* PushNotificationRepository.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PushNotificationRepository.swift; sourceTree = "<group>"; };
|
||||||
214C32BF44DF384FD6925E0B /* UserProfileChannelDonationView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UserProfileChannelDonationView.swift; sourceTree = "<group>"; };
|
214C32BF44DF384FD6925E0B /* UserProfileChannelDonationView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UserProfileChannelDonationView.swift; sourceTree = "<group>"; };
|
||||||
24266E32FC49D7C31610C2AD /* BannerCarousel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BannerCarousel.swift; sourceTree = "<group>"; };
|
24266E32FC49D7C31610C2AD /* BannerCarousel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BannerCarousel.swift; sourceTree = "<group>"; };
|
||||||
|
25C5D06E4F54C0ED7B7BD462 /* CreatorChannelFanTalkViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreatorChannelFanTalkViewModel.swift; sourceTree = "<group>"; };
|
||||||
25D89184D67BD63E46E90281 /* FontModifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FontModifier.swift; sourceTree = "<group>"; };
|
25D89184D67BD63E46E90281 /* FontModifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FontModifier.swift; sourceTree = "<group>"; };
|
||||||
26881ED30292E28D2E5F84CD /* CreatorProfileItem.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreatorProfileItem.swift; sourceTree = "<group>"; };
|
26881ED30292E28D2E5F84CD /* CreatorProfileItem.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreatorProfileItem.swift; sourceTree = "<group>"; };
|
||||||
348933CFF7DC1B9DB6E5D1A1 /* FollowAllButton.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FollowAllButton.swift; sourceTree = "<group>"; };
|
348933CFF7DC1B9DB6E5D1A1 /* FollowAllButton.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FollowAllButton.swift; sourceTree = "<group>"; };
|
||||||
375C41DF9D62AF5B9C3C1FAC /* DateParser.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DateParser.swift; sourceTree = "<group>"; };
|
375C41DF9D62AF5B9C3C1FAC /* DateParser.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DateParser.swift; sourceTree = "<group>"; };
|
||||||
|
3F1C1B0C73E8D92112E47610 /* CreatorChannelFanTalkTabResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreatorChannelFanTalkTabResponse.swift; sourceTree = "<group>"; };
|
||||||
3FA7A276BF4A47206ED702A7 /* PushNotificationListItemView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PushNotificationListItemView.swift; sourceTree = "<group>"; };
|
3FA7A276BF4A47206ED702A7 /* PushNotificationListItemView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PushNotificationListItemView.swift; sourceTree = "<group>"; };
|
||||||
4004671E2CDE08E6004DB9B5 /* PurchaseOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseOption.swift; sourceTree = "<group>"; };
|
4004671E2CDE08E6004DB9B5 /* PurchaseOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseOption.swift; sourceTree = "<group>"; };
|
||||||
40061F0D2B5D1E1D00130EF0 /* LivePaymentDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LivePaymentDialog.swift; sourceTree = "<group>"; };
|
40061F0D2B5D1E1D00130EF0 /* LivePaymentDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LivePaymentDialog.swift; sourceTree = "<group>"; };
|
||||||
@@ -2658,6 +2681,7 @@
|
|||||||
40FFB28D2E78517F004A0323 /* OriginalWorkCharactersPageResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OriginalWorkCharactersPageResponse.swift; sourceTree = "<group>"; };
|
40FFB28D2E78517F004A0323 /* OriginalWorkCharactersPageResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OriginalWorkCharactersPageResponse.swift; sourceTree = "<group>"; };
|
||||||
4339CA77E17ECD66D39D6379 /* V2vState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = V2vState.swift; sourceTree = "<group>"; };
|
4339CA77E17ECD66D39D6379 /* V2vState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = V2vState.swift; sourceTree = "<group>"; };
|
||||||
44A42B63A1021DD9F8AA3F30 /* DonationRankingPeriod.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DonationRankingPeriod.swift; sourceTree = "<group>"; };
|
44A42B63A1021DD9F8AA3F30 /* DonationRankingPeriod.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DonationRankingPeriod.swift; sourceTree = "<group>"; };
|
||||||
|
4A08977117E94D72914BB3FA /* CreatorChannelFanTalkEmptyView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreatorChannelFanTalkEmptyView.swift; sourceTree = "<group>"; };
|
||||||
4A5D253179845EF7577A1E01 /* GetCreatorDetailResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GetCreatorDetailResponse.swift; sourceTree = "<group>"; };
|
4A5D253179845EF7577A1E01 /* GetCreatorDetailResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GetCreatorDetailResponse.swift; sourceTree = "<group>"; };
|
||||||
54CF63CA56D78A23F93C21E5 /* AppDeepLinkHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AppDeepLinkHandler.swift; sourceTree = "<group>"; };
|
54CF63CA56D78A23F93C21E5 /* AppDeepLinkHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AppDeepLinkHandler.swift; sourceTree = "<group>"; };
|
||||||
5C4AF495E30F2DB00C2FCB36 /* V2vModels.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = V2vModels.swift; sourceTree = "<group>"; };
|
5C4AF495E30F2DB00C2FCB36 /* V2vModels.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = V2vModels.swift; sourceTree = "<group>"; };
|
||||||
@@ -2681,6 +2705,7 @@
|
|||||||
8A7650B281CE20A6F29F6253 /* ChannelDonationItemView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChannelDonationItemView.swift; sourceTree = "<group>"; };
|
8A7650B281CE20A6F29F6253 /* ChannelDonationItemView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChannelDonationItemView.swift; sourceTree = "<group>"; };
|
||||||
8E138A248EE3AFAFAF01E245 /* EntityInfo-SodaLive.generated.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "EntityInfo-SodaLive.generated.swift"; sourceTree = "<group>"; };
|
8E138A248EE3AFAFAF01E245 /* EntityInfo-SodaLive.generated.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "EntityInfo-SodaLive.generated.swift"; sourceTree = "<group>"; };
|
||||||
959B90DBB4BB5911AB10C442 /* Pods-SodaLive-dev.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SodaLive-dev.debug.xcconfig"; path = "Target Support Files/Pods-SodaLive-dev/Pods-SodaLive-dev.debug.xcconfig"; sourceTree = "<group>"; };
|
959B90DBB4BB5911AB10C442 /* Pods-SodaLive-dev.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SodaLive-dev.debug.xcconfig"; path = "Target Support Files/Pods-SodaLive-dev/Pods-SodaLive-dev.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
9C7923ABAD25E9CC4840EC94 /* CreatorChannelFanTalkTabView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreatorChannelFanTalkTabView.swift; sourceTree = "<group>"; };
|
||||||
A4AAC943F77B6FE089696909 /* UpdateContentPreferenceResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UpdateContentPreferenceResponse.swift; sourceTree = "<group>"; };
|
A4AAC943F77B6FE089696909 /* UpdateContentPreferenceResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UpdateContentPreferenceResponse.swift; sourceTree = "<group>"; };
|
||||||
A575789542808C0C87AC6D82 /* AiCharacterCard.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AiCharacterCard.swift; sourceTree = "<group>"; };
|
A575789542808C0C87AC6D82 /* AiCharacterCard.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AiCharacterCard.swift; sourceTree = "<group>"; };
|
||||||
AB63E7E31B75FF2E6F1C9073 /* LanguageSettingsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LanguageSettingsView.swift; sourceTree = "<group>"; };
|
AB63E7E31B75FF2E6F1C9073 /* LanguageSettingsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LanguageSettingsView.swift; sourceTree = "<group>"; };
|
||||||
@@ -2691,6 +2716,7 @@
|
|||||||
B8F49E8BB9BACFCA8878B70D /* MainTabBarButton.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainTabBarButton.swift; sourceTree = "<group>"; };
|
B8F49E8BB9BACFCA8878B70D /* MainTabBarButton.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainTabBarButton.swift; sourceTree = "<group>"; };
|
||||||
C07CBA176DB8BFF3F8D6BCEE /* TitleBar.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TitleBar.swift; sourceTree = "<group>"; };
|
C07CBA176DB8BFF3F8D6BCEE /* TitleBar.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TitleBar.swift; sourceTree = "<group>"; };
|
||||||
C1165E3F4CA1AB019FC85067 /* LanguageRepository.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LanguageRepository.swift; sourceTree = "<group>"; };
|
C1165E3F4CA1AB019FC85067 /* LanguageRepository.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LanguageRepository.swift; sourceTree = "<group>"; };
|
||||||
|
C40163E3E9DB325EA984D3C4 /* CreatorChannelFloatingIconButton.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreatorChannelFloatingIconButton.swift; sourceTree = "<group>"; };
|
||||||
C6B18C00DE256DCC53030630 /* Spacing.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Spacing.swift; sourceTree = "<group>"; };
|
C6B18C00DE256DCC53030630 /* Spacing.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Spacing.swift; sourceTree = "<group>"; };
|
||||||
C922F89A79C84E542AD7DE4F /* ExpandableTextView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExpandableTextView.swift; sourceTree = "<group>"; };
|
C922F89A79C84E542AD7DE4F /* ExpandableTextView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExpandableTextView.swift; sourceTree = "<group>"; };
|
||||||
CC0100012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelHomeResponse.swift; sourceTree = "<group>"; };
|
CC0100012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelHomeResponse.swift; sourceTree = "<group>"; };
|
||||||
@@ -2757,6 +2783,8 @@
|
|||||||
CE82912F2F111593BE42E33C /* CommunityPostCard.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommunityPostCard.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>"; };
|
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>"; };
|
||||||
D16C12DFB83FE164B38CC066 /* SectionTitle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SectionTitle.swift; sourceTree = "<group>"; };
|
D16C12DFB83FE164B38CC066 /* SectionTitle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SectionTitle.swift; sourceTree = "<group>"; };
|
||||||
|
D354C827944EC15C7D0FFBE0 /* CreatorChannelFanTalkListItem.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreatorChannelFanTalkListItem.swift; sourceTree = "<group>"; };
|
||||||
|
D3D241AC104728809A8E70C8 /* CreatorChannelFanTalkActionPopup.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreatorChannelFanTalkActionPopup.swift; sourceTree = "<group>"; };
|
||||||
D5420BBA14C00EC0D927BDAB /* MainViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainViewModel.swift; sourceTree = "<group>"; };
|
D5420BBA14C00EC0D927BDAB /* MainViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainViewModel.swift; sourceTree = "<group>"; };
|
||||||
D9DA056ECD3944E375300DBA /* GetChannelDonationListResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GetChannelDonationListResponse.swift; sourceTree = "<group>"; };
|
D9DA056ECD3944E375300DBA /* GetChannelDonationListResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GetChannelDonationListResponse.swift; sourceTree = "<group>"; };
|
||||||
DAA81E8424A575A0F8546D31 /* HomeTitleBar.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HomeTitleBar.swift; sourceTree = "<group>"; };
|
DAA81E8424A575A0F8546D31 /* HomeTitleBar.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HomeTitleBar.swift; sourceTree = "<group>"; };
|
||||||
@@ -2765,6 +2793,8 @@
|
|||||||
E56C8151F551B091940F16C4 /* V2vRepository.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = V2vRepository.swift; sourceTree = "<group>"; };
|
E56C8151F551B091940F16C4 /* V2vRepository.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = V2vRepository.swift; sourceTree = "<group>"; };
|
||||||
E61608194B084224DB6B78B5 /* TextTabBar.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TextTabBar.swift; sourceTree = "<group>"; };
|
E61608194B084224DB6B78B5 /* TextTabBar.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TextTabBar.swift; sourceTree = "<group>"; };
|
||||||
E92246F5C1254CBED72AD9AD /* AudioContentCard.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AudioContentCard.swift; sourceTree = "<group>"; };
|
E92246F5C1254CBED72AD9AD /* AudioContentCard.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AudioContentCard.swift; sourceTree = "<group>"; };
|
||||||
|
EDB71B1A61924B10599005FE /* CreatorChannelFanTalkCountBar.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreatorChannelFanTalkCountBar.swift; sourceTree = "<group>"; };
|
||||||
|
EF47BCA4DC51200D63D1E2D7 /* CreatorChannelFanTalkRepository.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreatorChannelFanTalkRepository.swift; sourceTree = "<group>"; };
|
||||||
F064EC9D5711B91979F45C05 /* YandexAdSupport.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YandexAdSupport.swift; sourceTree = "<group>"; };
|
F064EC9D5711B91979F45C05 /* YandexAdSupport.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YandexAdSupport.swift; sourceTree = "<group>"; };
|
||||||
F23D0834690CFE9FBDC10510 /* GetPushNotificationListResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GetPushNotificationListResponse.swift; sourceTree = "<group>"; };
|
F23D0834690CFE9FBDC10510 /* GetPushNotificationListResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GetPushNotificationListResponse.swift; sourceTree = "<group>"; };
|
||||||
F26CB443DA0ED4C45A18B258 /* PushNotificationApi.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PushNotificationApi.swift; sourceTree = "<group>"; };
|
F26CB443DA0ED4C45A18B258 /* PushNotificationApi.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PushNotificationApi.swift; sourceTree = "<group>"; };
|
||||||
@@ -2848,6 +2878,14 @@
|
|||||||
path = Repository;
|
path = Repository;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
09ACFA1137C51118E8147AB4 /* Models */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
3F1C1B0C73E8D92112E47610 /* CreatorChannelFanTalkTabResponse.swift */,
|
||||||
|
);
|
||||||
|
path = Models;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
0A06C1536829087772A1DC4F /* V2V */ = {
|
0A06C1536829087772A1DC4F /* V2V */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -5212,6 +5250,15 @@
|
|||||||
path = Pods;
|
path = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
67B3FF99E43E66E587F7E07B /* Repository */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
002435A7145D59CCCA8F76D1 /* CreatorChannelFanTalkApi.swift */,
|
||||||
|
EF47BCA4DC51200D63D1E2D7 /* CreatorChannelFanTalkRepository.swift */,
|
||||||
|
);
|
||||||
|
path = Repository;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
74A9B4B9FC9DC78523CA8FAA /* Language */ = {
|
74A9B4B9FC9DC78523CA8FAA /* Language */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -5233,6 +5280,17 @@
|
|||||||
path = Detail;
|
path = Detail;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
85110256B2C360449A456D4F /* Components */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
EDB71B1A61924B10599005FE /* CreatorChannelFanTalkCountBar.swift */,
|
||||||
|
D354C827944EC15C7D0FFBE0 /* CreatorChannelFanTalkListItem.swift */,
|
||||||
|
D3D241AC104728809A8E70C8 /* CreatorChannelFanTalkActionPopup.swift */,
|
||||||
|
4A08977117E94D72914BB3FA /* CreatorChannelFanTalkEmptyView.swift */,
|
||||||
|
);
|
||||||
|
path = Components;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
904977D239E14F12AECCD3EB /* ChannelDonation */ = {
|
904977D239E14F12AECCD3EB /* ChannelDonation */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -5265,6 +5323,18 @@
|
|||||||
path = Component;
|
path = Component;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
AA4588B91C0F73344C3DC4A4 /* FanTalk */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
09ACFA1137C51118E8147AB4 /* Models */,
|
||||||
|
67B3FF99E43E66E587F7E07B /* Repository */,
|
||||||
|
85110256B2C360449A456D4F /* Components */,
|
||||||
|
25C5D06E4F54C0ED7B7BD462 /* CreatorChannelFanTalkViewModel.swift */,
|
||||||
|
9C7923ABAD25E9CC4840EC94 /* CreatorChannelFanTalkTabView.swift */,
|
||||||
|
);
|
||||||
|
path = FanTalk;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
B068C16B9D7F3F80901622E8 /* Button */ = {
|
B068C16B9D7F3F80901622E8 /* Button */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -5362,6 +5432,7 @@
|
|||||||
CC05001B2FF0500000A5BBA3 /* Donation */,
|
CC05001B2FF0500000A5BBA3 /* Donation */,
|
||||||
CC0100082FF0100000A5BBA3 /* CreatorChannelView.swift */,
|
CC0100082FF0100000A5BBA3 /* CreatorChannelView.swift */,
|
||||||
CC0100092FF0100000A5BBA3 /* CreatorChannelViewModel.swift */,
|
CC0100092FF0100000A5BBA3 /* CreatorChannelViewModel.swift */,
|
||||||
|
AA4588B91C0F73344C3DC4A4 /* FanTalk */,
|
||||||
);
|
);
|
||||||
path = CreatorChannel;
|
path = CreatorChannel;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -5385,6 +5456,7 @@
|
|||||||
CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */,
|
CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */,
|
||||||
CC01002C2FF0100000A5BBA3 /* CreatorChannelSortContextPopup.swift */,
|
CC01002C2FF0100000A5BBA3 /* CreatorChannelSortContextPopup.swift */,
|
||||||
CC0200012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift */,
|
CC0200012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift */,
|
||||||
|
C40163E3E9DB325EA984D3C4 /* CreatorChannelFloatingIconButton.swift */,
|
||||||
);
|
);
|
||||||
path = Components;
|
path = Components;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -6905,6 +6977,16 @@
|
|||||||
938F4F60253B2E551A7474B6 /* MainTabBarView.swift in Sources */,
|
938F4F60253B2E551A7474B6 /* MainTabBarView.swift in Sources */,
|
||||||
E316EBA3E7125AFDA6CBE68D /* MainView.swift in Sources */,
|
E316EBA3E7125AFDA6CBE68D /* MainView.swift in Sources */,
|
||||||
FA5F18B3EE4CBD02D3807BD1 /* MainViewModel.swift in Sources */,
|
FA5F18B3EE4CBD02D3807BD1 /* MainViewModel.swift in Sources */,
|
||||||
|
CE935959F7A2BF7301F517DB /* CreatorChannelFanTalkTabResponse.swift in Sources */,
|
||||||
|
CD256952CC366FFB67557807 /* CreatorChannelFanTalkApi.swift in Sources */,
|
||||||
|
4466976AB5338188E0B6D536 /* CreatorChannelFanTalkRepository.swift in Sources */,
|
||||||
|
3BEBC2613F5CCEC2118B497B /* CreatorChannelFanTalkViewModel.swift in Sources */,
|
||||||
|
CFC28133D4D60FD1E436D97C /* CreatorChannelFanTalkTabView.swift in Sources */,
|
||||||
|
FD8075DBF9A40BA7544661FB /* CreatorChannelFanTalkCountBar.swift in Sources */,
|
||||||
|
6CDDA1EEA4D2E47A563FD687 /* CreatorChannelFanTalkListItem.swift in Sources */,
|
||||||
|
2BA6CE345486A0F75F7E75DA /* CreatorChannelFanTalkActionPopup.swift in Sources */,
|
||||||
|
1FA2B186FCA7B8B83648CF77 /* CreatorChannelFanTalkEmptyView.swift in Sources */,
|
||||||
|
69DBCDB25941100E1CFF8C50 /* CreatorChannelFloatingIconButton.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -7767,6 +7849,16 @@
|
|||||||
8E74CEEC5A35EF4D885FEE44 /* MainTabBarView.swift in Sources */,
|
8E74CEEC5A35EF4D885FEE44 /* MainTabBarView.swift in Sources */,
|
||||||
6B7A9ED264B5909F2E9F6E1C /* MainView.swift in Sources */,
|
6B7A9ED264B5909F2E9F6E1C /* MainView.swift in Sources */,
|
||||||
CAE33B321E81B312063B11E0 /* MainViewModel.swift in Sources */,
|
CAE33B321E81B312063B11E0 /* MainViewModel.swift in Sources */,
|
||||||
|
5AE12C82F7B160F075A5A6A8 /* CreatorChannelFanTalkTabResponse.swift in Sources */,
|
||||||
|
07F442FCA64E7F5D72E134CC /* CreatorChannelFanTalkApi.swift in Sources */,
|
||||||
|
DC3B6D07F71D7CFA83D91321 /* CreatorChannelFanTalkRepository.swift in Sources */,
|
||||||
|
B0DD3E23BFA368AD7007AF23 /* CreatorChannelFanTalkViewModel.swift in Sources */,
|
||||||
|
905BA213B28A010FDE2D9E0C /* CreatorChannelFanTalkTabView.swift in Sources */,
|
||||||
|
2DA2EA227CAA0E757E63A335 /* CreatorChannelFanTalkCountBar.swift in Sources */,
|
||||||
|
6A28B7059D970EC4E882EEA1 /* CreatorChannelFanTalkListItem.swift in Sources */,
|
||||||
|
4FDDF9E4DF4F003D30DDA8CE /* CreatorChannelFanTalkActionPopup.swift in Sources */,
|
||||||
|
2D761BB45AB6A332BE4690A1 /* CreatorChannelFanTalkEmptyView.swift in Sources */,
|
||||||
|
5B9BFA224A49A5AF0757FDF7 /* CreatorChannelFloatingIconButton.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ struct CreatorChannelFloatingActionMenu: View {
|
|||||||
let onTapAudioContent: () -> Void
|
let onTapAudioContent: () -> Void
|
||||||
let onTapCreateLive: () -> Void
|
let onTapCreateLive: () -> Void
|
||||||
|
|
||||||
private let buttonSize: CGFloat = 66
|
|
||||||
private let animation = Animation.interpolatingSpring(mass: 1, stiffness: 256, damping: 24, initialVelocity: 0)
|
private let animation = Animation.interpolatingSpring(mass: 1, stiffness: 256, damping: 24, initialVelocity: 0)
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -24,28 +23,28 @@ struct CreatorChannelFloatingActionMenu: View {
|
|||||||
|
|
||||||
VStack(spacing: SodaSpacing.s14) {
|
VStack(spacing: SodaSpacing.s14) {
|
||||||
if isPresented {
|
if isPresented {
|
||||||
floatingButton(
|
CreatorChannelFloatingIconButton(
|
||||||
imageName: "ic_new_upload_community_post",
|
imageName: "ic_new_upload_community_post",
|
||||||
backgroundColor: Color.soda400,
|
backgroundColor: Color.soda400,
|
||||||
accessibilityLabel: I18n.CreatorChannelHome.uploadCommunityPost,
|
accessibilityLabel: I18n.CreatorChannelHome.uploadCommunityPost,
|
||||||
action: onTapCommunityPost
|
action: onTapCommunityPost
|
||||||
)
|
)
|
||||||
|
|
||||||
floatingButton(
|
CreatorChannelFloatingIconButton(
|
||||||
imageName: "ic_new_upload_audio",
|
imageName: "ic_new_upload_audio",
|
||||||
backgroundColor: Color.soda400,
|
backgroundColor: Color.soda400,
|
||||||
accessibilityLabel: I18n.CreatorChannelHome.uploadAudioContent,
|
accessibilityLabel: I18n.CreatorChannelHome.uploadAudioContent,
|
||||||
action: onTapAudioContent
|
action: onTapAudioContent
|
||||||
)
|
)
|
||||||
|
|
||||||
floatingButton(
|
CreatorChannelFloatingIconButton(
|
||||||
imageName: "ic_new_create_live",
|
imageName: "ic_new_create_live",
|
||||||
backgroundColor: Color.soda800,
|
backgroundColor: Color.soda800,
|
||||||
accessibilityLabel: I18n.CreatorChannelHome.createLive,
|
accessibilityLabel: I18n.CreatorChannelHome.createLive,
|
||||||
action: onTapCreateLive
|
action: onTapCreateLive
|
||||||
)
|
)
|
||||||
|
|
||||||
floatingButton(
|
CreatorChannelFloatingIconButton(
|
||||||
imageName: "ic_new_x_black",
|
imageName: "ic_new_x_black",
|
||||||
backgroundColor: Color.white,
|
backgroundColor: Color.white,
|
||||||
accessibilityLabel: I18n.CreatorChannelHome.closeFloatingMenu
|
accessibilityLabel: I18n.CreatorChannelHome.closeFloatingMenu
|
||||||
@@ -55,7 +54,7 @@ struct CreatorChannelFloatingActionMenu: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
floatingButton(
|
CreatorChannelFloatingIconButton(
|
||||||
imageName: "ic_plus_no_bg",
|
imageName: "ic_plus_no_bg",
|
||||||
backgroundColor: Color.soda400,
|
backgroundColor: Color.soda400,
|
||||||
accessibilityLabel: "FAB"
|
accessibilityLabel: "FAB"
|
||||||
@@ -72,23 +71,6 @@ struct CreatorChannelFloatingActionMenu: View {
|
|||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func floatingButton(
|
|
||||||
imageName: String,
|
|
||||||
backgroundColor: Color,
|
|
||||||
accessibilityLabel: String,
|
|
||||||
action: @escaping () -> Void
|
|
||||||
) -> some View {
|
|
||||||
Button(action: action) {
|
|
||||||
Image(imageName)
|
|
||||||
.resizable()
|
|
||||||
.frame(width: 38, height: 38)
|
|
||||||
.frame(width: buttonSize, height: buttonSize)
|
|
||||||
.background(backgroundColor)
|
|
||||||
.clipShape(Circle())
|
|
||||||
}
|
|
||||||
.accessibilityLabel(accessibilityLabel)
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CreatorChannelFloatingActionMenu_Previews: PreviewProvider {
|
struct CreatorChannelFloatingActionMenu_Previews: PreviewProvider {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct CreatorChannelFloatingIconButton: View {
|
||||||
|
let imageName: String
|
||||||
|
let backgroundColor: Color
|
||||||
|
let accessibilityLabel: String
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: action) {
|
||||||
|
Image(imageName)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 38, height: 38)
|
||||||
|
.frame(width: 66, height: 66)
|
||||||
|
.background(backgroundColor)
|
||||||
|
.clipShape(Circle())
|
||||||
|
}
|
||||||
|
.accessibilityLabel(accessibilityLabel)
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ struct CreatorChannelView: View {
|
|||||||
@StateObject private var viewModel = CreatorChannelViewModel()
|
@StateObject private var viewModel = CreatorChannelViewModel()
|
||||||
@StateObject private var channelDonationViewModel = ChannelDonationViewModel()
|
@StateObject private var channelDonationViewModel = ChannelDonationViewModel()
|
||||||
@StateObject private var donationViewModel = CreatorChannelDonationViewModel()
|
@StateObject private var donationViewModel = CreatorChannelDonationViewModel()
|
||||||
|
@StateObject private var fanTalkViewModel = CreatorChannelFanTalkViewModel()
|
||||||
@StateObject private var liveViewModel = LiveViewModel()
|
@StateObject private var liveViewModel = LiveViewModel()
|
||||||
@StateObject private var mypageViewModel = MyPageViewModel()
|
@StateObject private var mypageViewModel = MyPageViewModel()
|
||||||
@AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
|
@AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
|
||||||
@@ -121,6 +122,26 @@ struct CreatorChannelView: View {
|
|||||||
.padding(.trailing, SodaSpacing.s14)
|
.padding(.trailing, SodaSpacing.s14)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if viewModel.selectedTab == .fanTalk && showFanTalkWrite && !viewModel.isLoading {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
CreatorChannelFloatingIconButton(
|
||||||
|
imageName: "ic_plus_no_bg",
|
||||||
|
backgroundColor: Color.soda400,
|
||||||
|
accessibilityLabel: I18n.CreatorChannelHome.fanTalk,
|
||||||
|
action: showFanTalkWriteView
|
||||||
|
)
|
||||||
|
.padding(.trailing, SodaSpacing.s14)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ignoresSafeArea(.container, edges: .bottom)
|
||||||
|
.padding(.bottom, SodaSpacing.s14)
|
||||||
|
}
|
||||||
|
|
||||||
if isShowAuthConfirmView {
|
if isShowAuthConfirmView {
|
||||||
authConfirmDialog
|
authConfirmDialog
|
||||||
}
|
}
|
||||||
@@ -220,6 +241,10 @@ struct CreatorChannelView: View {
|
|||||||
isOwnCreatorChannel == false && (donationViewModel.donationCount != 0 || donationViewModel.rankings.isEmpty == false)
|
isOwnCreatorChannel == false && (donationViewModel.donationCount != 0 || donationViewModel.rankings.isEmpty == false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var showFanTalkWrite: Bool {
|
||||||
|
isOwnCreatorChannel == false && fanTalkViewModel.fanTalkCount > 0
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var selectedTabContent: some View {
|
private var selectedTabContent: some View {
|
||||||
if viewModel.selectedTab == .live {
|
if viewModel.selectedTab == .live {
|
||||||
@@ -249,6 +274,13 @@ struct CreatorChannelView: View {
|
|||||||
onTapViewAll: showDonationRankingAll,
|
onTapViewAll: showDonationRankingAll,
|
||||||
onTapDonate: showDonationDialog
|
onTapDonate: showDonationDialog
|
||||||
)
|
)
|
||||||
|
} else if viewModel.selectedTab == .fanTalk {
|
||||||
|
CreatorChannelFanTalkTabView(
|
||||||
|
creatorId: creatorId,
|
||||||
|
isOwnCreatorChannel: isOwnCreatorChannel,
|
||||||
|
viewModel: fanTalkViewModel,
|
||||||
|
onTapWrite: showFanTalkWriteView
|
||||||
|
)
|
||||||
} else if viewModel.selectedTab != .home {
|
} else if viewModel.selectedTab != .home {
|
||||||
CreatorChannelPlaceholderTabView(title: viewModel.selectedTab.title)
|
CreatorChannelPlaceholderTabView(title: viewModel.selectedTab.title)
|
||||||
} else if let response = viewModel.response {
|
} else if let response = viewModel.response {
|
||||||
@@ -461,6 +493,8 @@ struct CreatorChannelView: View {
|
|||||||
channelDonationViewModel.setCreatorId(creatorId, shouldFetch: false)
|
channelDonationViewModel.setCreatorId(creatorId, shouldFetch: false)
|
||||||
isShowChannelDonationDialog = true
|
isShowChannelDonationDialog = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func showFanTalkWriteView() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CreatorChannelView_Previews: PreviewProvider {
|
struct CreatorChannelView_Previews: PreviewProvider {
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct CreatorChannelFanTalkActionPopup: View {
|
||||||
|
let isMine: Bool
|
||||||
|
let isOwnCreatorChannel: Bool
|
||||||
|
let onTapEdit: () -> Void
|
||||||
|
let onTapDelete: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
if isMine {
|
||||||
|
actionButton(title: "수정하기", action: onTapEdit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isMine || isOwnCreatorChannel {
|
||||||
|
actionButton(title: "삭제하기", action: onTapDelete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(Color.gray900)
|
||||||
|
.cornerRadius(14)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||||
|
.stroke(Color.gray700, lineWidth: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func actionButton(title: String, action: @escaping () -> Void) -> some View {
|
||||||
|
Button(action: action) {
|
||||||
|
Text(title)
|
||||||
|
.appFont(size: 14, weight: .medium)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.lineLimit(1)
|
||||||
|
.frame(minWidth: 80, alignment: .leading)
|
||||||
|
.padding(.horizontal, SodaSpacing.s12)
|
||||||
|
.padding(.vertical, SodaSpacing.s8)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct CreatorChannelFanTalkCountBar: View {
|
||||||
|
let fanTalkCount: Int
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: SodaSpacing.s4) {
|
||||||
|
Text("전체")
|
||||||
|
.appFont(size: 16, weight: .medium)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
|
||||||
|
Text("\(fanTalkCount)")
|
||||||
|
.appFont(size: 16, weight: .medium)
|
||||||
|
.foregroundColor(Color.gray500)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.horizontal, SodaSpacing.s14)
|
||||||
|
.frame(height: 52)
|
||||||
|
.background(Color.black)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct CreatorChannelFanTalkEmptyView: View {
|
||||||
|
let showsWriteButton: Bool
|
||||||
|
let onTapWrite: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: SodaSpacing.s20) {
|
||||||
|
Text("아직 응원이 없습니다.\n처음으로 크리에이터를 응원해 보세요!")
|
||||||
|
.appFont(size: 16, weight: .medium)
|
||||||
|
.foregroundColor(Color.gray500)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.lineSpacing(SodaSpacing.s4)
|
||||||
|
|
||||||
|
if showsWriteButton {
|
||||||
|
Button(action: onTapWrite) {
|
||||||
|
HStack(spacing: SodaSpacing.s6) {
|
||||||
|
Image("ic_plus_no_bg")
|
||||||
|
.resizable()
|
||||||
|
.renderingMode(.template)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
|
||||||
|
Text("응원 남기기")
|
||||||
|
.appFont(size: 16, weight: .medium)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, SodaSpacing.s20)
|
||||||
|
.frame(height: 44)
|
||||||
|
.background(Color.soda400)
|
||||||
|
.clipShape(Capsule())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 48)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct CreatorChannelFanTalkListItem: View {
|
||||||
|
let fanTalk: CreatorChannelFanTalkItemResponse
|
||||||
|
let isMine: Bool
|
||||||
|
let isOwnCreatorChannel: Bool
|
||||||
|
let onTapReport: () -> Void
|
||||||
|
let onTapMore: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
VStack(alignment: .leading, spacing: SodaSpacing.s12) {
|
||||||
|
header
|
||||||
|
|
||||||
|
Text(fanTalk.content)
|
||||||
|
.appFont(size: 16, weight: .regular)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.leading, 50)
|
||||||
|
|
||||||
|
if fanTalk.creatorReplies.isEmpty == false {
|
||||||
|
replies
|
||||||
|
.padding(.leading, 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, SodaSpacing.s14)
|
||||||
|
.padding(.vertical, SodaSpacing.s16)
|
||||||
|
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.gray800)
|
||||||
|
.frame(height: 1)
|
||||||
|
}
|
||||||
|
.background(Color.black)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var header: some View {
|
||||||
|
HStack(alignment: .top, spacing: SodaSpacing.s8) {
|
||||||
|
DownsampledKFImage(
|
||||||
|
url: URL(string: fanTalk.writerProfileImageUrl),
|
||||||
|
size: CGSize(width: 42, height: 42)
|
||||||
|
)
|
||||||
|
.background(Color.white.opacity(0.6))
|
||||||
|
.clipShape(Circle())
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(fanTalk.writerNickname)
|
||||||
|
.appFont(size: 14, weight: .medium)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
|
||||||
|
Text(fanTalk.relativeTimeText())
|
||||||
|
.appFont(size: 14, weight: .regular)
|
||||||
|
.foregroundColor(Color.gray500)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
|
trailingAction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var trailingAction: some View {
|
||||||
|
Group {
|
||||||
|
if isMine || isOwnCreatorChannel {
|
||||||
|
Button(action: onTapMore) {
|
||||||
|
Image("ic_new_more")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
} else {
|
||||||
|
Button(action: onTapReport) {
|
||||||
|
Text("신고")
|
||||||
|
.appFont(size: 13, weight: .medium)
|
||||||
|
.foregroundColor(Color.gray500)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var replies: some View {
|
||||||
|
VStack(alignment: .leading, spacing: SodaSpacing.s8) {
|
||||||
|
ForEach(fanTalk.creatorReplies) { reply in
|
||||||
|
HStack(alignment: .top, spacing: SodaSpacing.s8) {
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.gray800)
|
||||||
|
.frame(width: 2)
|
||||||
|
|
||||||
|
replyCard(reply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func replyCard(_ reply: CreatorChannelFanTalkReplyResponse) -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: SodaSpacing.s8) {
|
||||||
|
HStack(alignment: .center, spacing: SodaSpacing.s6) {
|
||||||
|
DownsampledKFImage(
|
||||||
|
url: URL(string: reply.writerProfileImageUrl),
|
||||||
|
size: CGSize(width: 20, height: 20)
|
||||||
|
)
|
||||||
|
.background(Color.white.opacity(0.6))
|
||||||
|
.clipShape(Circle())
|
||||||
|
|
||||||
|
Text(reply.writerNickname)
|
||||||
|
.appFont(size: 13, weight: .medium)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.lineLimit(1)
|
||||||
|
|
||||||
|
Text(reply.relativeTimeText())
|
||||||
|
.appFont(size: 14, weight: .regular)
|
||||||
|
.foregroundColor(Color.gray500)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(reply.content)
|
||||||
|
.appFont(size: 16, weight: .regular)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
.padding(SodaSpacing.s12)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.background(Color.gray900)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CreatorChannelFanTalkItemResponse {
|
||||||
|
func relativeTimeText(now: Date = Date()) -> String {
|
||||||
|
DateParser.relativeTimeText(fromUTC: createdAtUtc, fallback: createdAtUtc, now: now)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CreatorChannelFanTalkReplyResponse {
|
||||||
|
func relativeTimeText(now: Date = Date()) -> String {
|
||||||
|
DateParser.relativeTimeText(fromUTC: createdAtUtc, fallback: createdAtUtc, now: now)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct CreatorChannelFanTalkTabView: View {
|
||||||
|
let creatorId: Int
|
||||||
|
let isOwnCreatorChannel: Bool
|
||||||
|
@ObservedObject var viewModel: CreatorChannelFanTalkViewModel
|
||||||
|
let onTapWrite: () -> Void
|
||||||
|
|
||||||
|
@State private var actionPopupFanTalk: CreatorChannelFanTalkItemResponse?
|
||||||
|
@State private var fanTalkItemFrames = [Int: CGRect]()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack(alignment: .topTrailing) {
|
||||||
|
content
|
||||||
|
|
||||||
|
actionPopupLayer
|
||||||
|
.zIndex(1)
|
||||||
|
}
|
||||||
|
.coordinateSpace(name: Self.coordinateSpaceName)
|
||||||
|
.background(Color.black)
|
||||||
|
.onPreferenceChange(CreatorChannelFanTalkItemFramePreferenceKey.self) { frames in
|
||||||
|
fanTalkItemFrames.merge(frames) { _, new in new }
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
if viewModel.hasLoaded == false {
|
||||||
|
viewModel.fetchFirstPage(creatorId: creatorId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 1)
|
||||||
|
.overlay(reportDialog)
|
||||||
|
.overlay(deleteDialog)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var content: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
CreatorChannelFanTalkCountBar(fanTalkCount: viewModel.fanTalkCount)
|
||||||
|
|
||||||
|
if isEmptyState {
|
||||||
|
CreatorChannelFanTalkEmptyView(
|
||||||
|
showsWriteButton: isOwnCreatorChannel == false,
|
||||||
|
onTapWrite: onTapWrite
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LazyVStack(spacing: 0) {
|
||||||
|
ForEach(viewModel.fanTalks) { fanTalk in
|
||||||
|
fanTalkItem(fanTalk)
|
||||||
|
.onAppear {
|
||||||
|
viewModel.fetchNextPageIfNeeded(
|
||||||
|
creatorId: creatorId,
|
||||||
|
currentItem: fanTalk
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.isLoadingNextPage {
|
||||||
|
ProgressView()
|
||||||
|
.padding(.vertical, SodaSpacing.s20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.bottom, SodaSpacing.s20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fanTalkItem(_ fanTalk: CreatorChannelFanTalkItemResponse) -> some View {
|
||||||
|
let isMine = fanTalk.writerId == UserDefaults.int(forKey: .userId)
|
||||||
|
|
||||||
|
return CreatorChannelFanTalkListItem(
|
||||||
|
fanTalk: fanTalk,
|
||||||
|
isMine: isMine,
|
||||||
|
isOwnCreatorChannel: isOwnCreatorChannel,
|
||||||
|
onTapReport: {
|
||||||
|
actionPopupFanTalk = nil
|
||||||
|
viewModel.presentReportDialog(fanTalkId: fanTalk.fanTalkId)
|
||||||
|
},
|
||||||
|
onTapMore: {
|
||||||
|
actionPopupFanTalk = actionPopupFanTalk?.fanTalkId == fanTalk.fanTalkId ? nil : fanTalk
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.background {
|
||||||
|
GeometryReader { proxy in
|
||||||
|
Color.clear.preference(
|
||||||
|
key: CreatorChannelFanTalkItemFramePreferenceKey.self,
|
||||||
|
value: [fanTalk.fanTalkId: proxy.frame(in: .named(Self.coordinateSpaceName))]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var actionPopupLayer: some View {
|
||||||
|
if let fanTalk = actionPopupFanTalk,
|
||||||
|
let itemFrame = fanTalkItemFrames[fanTalk.fanTalkId] {
|
||||||
|
let isMine = fanTalk.writerId == UserDefaults.int(forKey: .userId)
|
||||||
|
|
||||||
|
CreatorChannelFanTalkActionPopup(
|
||||||
|
isMine: isMine,
|
||||||
|
isOwnCreatorChannel: isOwnCreatorChannel,
|
||||||
|
onTapEdit: {
|
||||||
|
actionPopupFanTalk = nil
|
||||||
|
},
|
||||||
|
onTapDelete: {
|
||||||
|
actionPopupFanTalk = nil
|
||||||
|
viewModel.presentDeleteDialog(fanTalkId: fanTalk.fanTalkId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.padding(.top, itemFrame.minY + 44)
|
||||||
|
.padding(.trailing, SodaSpacing.s14)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isEmptyState: Bool {
|
||||||
|
guard viewModel.hasLoaded else { return false }
|
||||||
|
return viewModel.fanTalkCount == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var reportDialog: some View {
|
||||||
|
if viewModel.isShowReportDialog {
|
||||||
|
CheersReportDialogView(
|
||||||
|
isShowing: $viewModel.isShowReportDialog,
|
||||||
|
confirmAction: { reason in
|
||||||
|
viewModel.report(reason: reason)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var deleteDialog: some View {
|
||||||
|
if viewModel.isShowDeleteDialog {
|
||||||
|
SodaDialog(
|
||||||
|
title: I18n.MemberChannel.cheersDeleteTitle,
|
||||||
|
desc: I18n.Common.confirmDeleteQuestion,
|
||||||
|
confirmButtonTitle: I18n.Common.delete,
|
||||||
|
confirmButtonAction: {
|
||||||
|
viewModel.isShowDeleteDialog = false
|
||||||
|
viewModel.deleteSelectedFanTalk(creatorId: creatorId)
|
||||||
|
},
|
||||||
|
cancelButtonTitle: I18n.Common.cancel,
|
||||||
|
cancelButtonAction: {
|
||||||
|
viewModel.isShowDeleteDialog = false
|
||||||
|
viewModel.selectedFanTalkId = 0
|
||||||
|
},
|
||||||
|
textAlignment: .center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let coordinateSpaceName = "CreatorChannelFanTalkTabView"
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct CreatorChannelFanTalkItemFramePreferenceKey: PreferenceKey {
|
||||||
|
static var defaultValue: [Int: CGRect] = [:]
|
||||||
|
|
||||||
|
static func reduce(value: inout [Int: CGRect], nextValue: () -> [Int: CGRect]) {
|
||||||
|
value.merge(nextValue()) { _, new in new }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
final class CreatorChannelFanTalkViewModel: ObservableObject {
|
||||||
|
private let repository = CreatorChannelFanTalkRepository()
|
||||||
|
private let reportRepository = ReportRepository()
|
||||||
|
private let explorerRepository = ExplorerRepository()
|
||||||
|
private var subscription = Set<AnyCancellable>()
|
||||||
|
private let pageSize = 20
|
||||||
|
private var latestRequestId = 0
|
||||||
|
|
||||||
|
@Published var isLoading = false
|
||||||
|
@Published var isLoadingNextPage = false
|
||||||
|
@Published var response: CreatorChannelFanTalkTabResponse?
|
||||||
|
@Published var fanTalks = [CreatorChannelFanTalkItemResponse]()
|
||||||
|
@Published var page = 0
|
||||||
|
@Published var size = 20
|
||||||
|
@Published var hasNext = false
|
||||||
|
@Published var hasLoaded = false
|
||||||
|
@Published var errorMessage = ""
|
||||||
|
@Published var isShowPopup = false
|
||||||
|
@Published var isShowReportDialog = false
|
||||||
|
@Published var isShowDeleteDialog = false
|
||||||
|
@Published var selectedFanTalkId = 0
|
||||||
|
|
||||||
|
var fanTalkCount: Int {
|
||||||
|
response?.fanTalkCount ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchFirstPage(creatorId: Int) {
|
||||||
|
fetchFanTalks(creatorId: creatorId, page: 0, isNextPage: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchNextPageIfNeeded(creatorId: Int, currentItem: CreatorChannelFanTalkItemResponse) {
|
||||||
|
guard let lastFanTalk = fanTalks.last else { return }
|
||||||
|
guard lastFanTalk.fanTalkId == currentItem.fanTalkId else { return }
|
||||||
|
guard hasNext, isLoadingNextPage == false, isLoading == false else { return }
|
||||||
|
|
||||||
|
fetchFanTalks(creatorId: creatorId, page: page + 1, isNextPage: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func presentReportDialog(fanTalkId: Int) {
|
||||||
|
selectedFanTalkId = fanTalkId
|
||||||
|
isShowReportDialog = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func presentDeleteDialog(fanTalkId: Int) {
|
||||||
|
selectedFanTalkId = fanTalkId
|
||||||
|
isShowDeleteDialog = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func report(reason: String) {
|
||||||
|
guard selectedFanTalkId > 0 else { return }
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
let request = ReportRequest(type: .CHEERS, reason: reason, reportedMemberId: nil, cheersId: selectedFanTalkId, audioContentId: nil)
|
||||||
|
|
||||||
|
reportRepository.report(request: request)
|
||||||
|
.sink { [weak self] result in
|
||||||
|
guard let self 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 }
|
||||||
|
|
||||||
|
self.isLoading = false
|
||||||
|
self.selectedFanTalkId = 0
|
||||||
|
|
||||||
|
do {
|
||||||
|
let decoded = try JSONDecoder().decode(ApiResponseWithoutData.self, from: response.data)
|
||||||
|
self.errorMessage = decoded.message ?? I18n.Common.commonError
|
||||||
|
self.isShowPopup = true
|
||||||
|
} catch {
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
self.errorMessage = I18n.Common.commonError
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &subscription)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteSelectedFanTalk(creatorId: Int) {
|
||||||
|
guard selectedFanTalkId > 0 else { return }
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
explorerRepository.modifyCheers(cheersId: selectedFanTalkId, content: nil, isActive: false)
|
||||||
|
.sink { [weak self] result in
|
||||||
|
guard let self 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 }
|
||||||
|
|
||||||
|
self.isLoading = false
|
||||||
|
self.selectedFanTalkId = 0
|
||||||
|
|
||||||
|
do {
|
||||||
|
let decoded = try JSONDecoder().decode(ApiResponseWithoutData.self, from: response.data)
|
||||||
|
|
||||||
|
if decoded.success {
|
||||||
|
self.fetchFirstPage(creatorId: creatorId)
|
||||||
|
} else {
|
||||||
|
self.errorMessage = decoded.message ?? I18n.Common.commonError
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
self.errorMessage = I18n.Common.commonError
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &subscription)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fetchFanTalks(creatorId: Int, page requestPage: Int, isNextPage: Bool) {
|
||||||
|
latestRequestId += 1
|
||||||
|
let requestId = latestRequestId
|
||||||
|
|
||||||
|
if isNextPage {
|
||||||
|
isLoadingNextPage = true
|
||||||
|
} else {
|
||||||
|
isLoading = true
|
||||||
|
}
|
||||||
|
|
||||||
|
repository.getFanTalks(creatorId: creatorId, page: requestPage, size: pageSize)
|
||||||
|
.sink { [weak self] result in
|
||||||
|
guard let self else { return }
|
||||||
|
guard requestId == self.latestRequestId else { return }
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .finished:
|
||||||
|
DEBUG_LOG("finish")
|
||||||
|
self.isLoading = false
|
||||||
|
self.isLoadingNextPage = false
|
||||||
|
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 }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let decoded = try JSONDecoder().decode(ApiResponse<CreatorChannelFanTalkTabResponse>.self, from: response.data)
|
||||||
|
|
||||||
|
if let data = decoded.data, decoded.success {
|
||||||
|
self.applySuccessState(data, isNextPage: isNextPage)
|
||||||
|
self.hasLoaded = true
|
||||||
|
} 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.isLoading = false
|
||||||
|
self.isLoadingNextPage = false
|
||||||
|
}
|
||||||
|
.store(in: &subscription)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applySuccessState(_ data: CreatorChannelFanTalkTabResponse, isNextPage: Bool) {
|
||||||
|
response = data
|
||||||
|
page = data.page
|
||||||
|
size = data.size
|
||||||
|
hasNext = data.hasNext
|
||||||
|
|
||||||
|
if isNextPage {
|
||||||
|
fanTalks.append(contentsOf: data.fanTalks)
|
||||||
|
} else {
|
||||||
|
fanTalks = data.fanTalks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyFailureState() {
|
||||||
|
errorMessage = I18n.Common.commonError
|
||||||
|
isShowPopup = true
|
||||||
|
isLoading = false
|
||||||
|
isLoadingNextPage = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct CreatorChannelFanTalkTabResponse: Decodable {
|
||||||
|
let fanTalkCount: Int
|
||||||
|
let fanTalks: [CreatorChannelFanTalkItemResponse]
|
||||||
|
let page: Int
|
||||||
|
let size: Int
|
||||||
|
let hasNext: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CreatorChannelFanTalkItemResponse: Decodable, Identifiable {
|
||||||
|
let fanTalkId: Int
|
||||||
|
let writerId: Int
|
||||||
|
let writerNickname: String
|
||||||
|
let writerProfileImageUrl: String
|
||||||
|
let content: String
|
||||||
|
let createdAtUtc: String
|
||||||
|
let creatorReplies: [CreatorChannelFanTalkReplyResponse]
|
||||||
|
|
||||||
|
var id: Int { fanTalkId }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CreatorChannelFanTalkReplyResponse: Decodable, Identifiable {
|
||||||
|
let fanTalkId: Int
|
||||||
|
let writerId: Int
|
||||||
|
let writerNickname: String
|
||||||
|
let writerProfileImageUrl: String
|
||||||
|
let content: String
|
||||||
|
let createdAtUtc: String
|
||||||
|
|
||||||
|
var id: Int { fanTalkId }
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
import Moya
|
||||||
|
|
||||||
|
enum CreatorChannelFanTalkApi {
|
||||||
|
case getFanTalks(creatorId: Int, page: Int, size: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CreatorChannelFanTalkApi: TargetType {
|
||||||
|
var baseURL: URL {
|
||||||
|
return URL(string: BASE_URL)!
|
||||||
|
}
|
||||||
|
|
||||||
|
var path: String {
|
||||||
|
switch self {
|
||||||
|
case .getFanTalks(let creatorId, _, _):
|
||||||
|
return "/api/v2/creator-channels/\(creatorId)/fan-talks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var method: Moya.Method {
|
||||||
|
switch self {
|
||||||
|
case .getFanTalks:
|
||||||
|
return .get
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var task: Moya.Task {
|
||||||
|
switch self {
|
||||||
|
case .getFanTalks(_, let page, let size):
|
||||||
|
return .requestParameters(
|
||||||
|
parameters: [
|
||||||
|
"page": page,
|
||||||
|
"size": size
|
||||||
|
],
|
||||||
|
encoding: URLEncoding.queryString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var headers: [String: String]? {
|
||||||
|
return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
import CombineMoya
|
||||||
|
import Moya
|
||||||
|
|
||||||
|
final class CreatorChannelFanTalkRepository {
|
||||||
|
private let api = MoyaProvider<CreatorChannelFanTalkApi>()
|
||||||
|
|
||||||
|
func getFanTalks(creatorId: Int, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.getFanTalks(creatorId: creatorId, page: page, size: size))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,11 +53,6 @@ private struct CreatorChannelFanTalkCard: View {
|
|||||||
.truncationMode(.tail)
|
.truncationMode(.tail)
|
||||||
.frame(maxWidth: .infinity, minHeight: 40, alignment: .leading)
|
.frame(maxWidth: .infinity, minHeight: 40, alignment: .leading)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
Image(systemName: "chevron.down")
|
|
||||||
.font(.system(size: 12, weight: .medium))
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.frame(width: 18, height: 18)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(SodaSpacing.s14)
|
.padding(SodaSpacing.s14)
|
||||||
|
|||||||
228
docs/20260704_크리에이터_채널_팬Talk_탭/plan-task.md
Normal file
228
docs/20260704_크리에이터_채널_팬Talk_탭/plan-task.md
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
# 크리에이터 채널 팬Talk 탭 구현 계획
|
||||||
|
|
||||||
|
## 기준 문서
|
||||||
|
|
||||||
|
- PRD: `docs/20260704_크리에이터_채널_팬Talk_탭/prd.md`
|
||||||
|
- 기존 크리에이터 채널 홈 PRD: `docs/20260701_크리에이터_채널_홈/prd.md`
|
||||||
|
- 기존 크리에이터 채널 후원 탭 구현 계획: `docs/20260704_크리에이터_채널_후원_탭/plan-task.md`
|
||||||
|
- 코드 스타일: `docs/agent-guides/code-style.md`
|
||||||
|
- 빌드/검증: `docs/agent-guides/build-test-verification.md`
|
||||||
|
- Figma 전체 화면: `290:9139`, `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-9139&m=dev`
|
||||||
|
- Figma 아이템 뷰(답글 없음): `290:9145`, `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-9145&m=dev`
|
||||||
|
- Figma 아이템 뷰(답글 있음): `290:9147`, `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-9147&m=dev`
|
||||||
|
- Figma 더보기 anchored context popup: `666:36114`, `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=666-36114&m=dev`
|
||||||
|
- Figma Empty: `290:9001`, `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-9001&m=dev`
|
||||||
|
|
||||||
|
## 구현 원칙
|
||||||
|
|
||||||
|
- 기존 `CreatorChannelView`의 공통 shell, header, sticky tab-bar, title bar 동작은 유지한다.
|
||||||
|
- `CreatorChannelTab.fanTalk` 콘텐츠만 신규 `CreatorChannelFanTalkTabView`로 교체한다.
|
||||||
|
- 팬Talk API 모델은 홈 탭 모델과 타입명 충돌이 나지 않도록 분리한다.
|
||||||
|
- sort-bar는 정렬 없이 `전체 {fanTalkCount}`만 표시한다.
|
||||||
|
- 신고는 기존 `CheersReportDialogView`와 `ReportRepository.report`의 `ReportType.CHEERS` 흐름을 재사용한다.
|
||||||
|
- 삭제는 기존 `ExplorerRepository.modifyCheers(cheersId:content:isActive:)`에 `isActive=false`를 전달하는 흐름을 재사용한다.
|
||||||
|
- 더보기 버튼은 `ic_new_more`를 사용한다.
|
||||||
|
- 내가 쓴 글이면 `수정하기`, `삭제하기`를 표시하고, 내가 쓴 글이 아니지만 내 채널이면 `삭제하기`만 표시한다.
|
||||||
|
- 글쓰기와 수정하기 action은 신규 글쓰기 페이지 구현 전까지 no-op placeholder로 둔다.
|
||||||
|
- 내 채널인 경우 우측하단 글쓰기 floating button과 empty view 내부 `응원 남기기` 버튼을 모두 표시하지 않는다.
|
||||||
|
- 우측하단 plus button은 `CreatorChannelFloatingActionMenu`의 private `floatingButton`과 동일한 UI이므로, 팬Talk 전용으로 별도 구현하지 않고 `CreatorChannel/Components` 하위 공용 icon floating button으로 추출해 재사용한다.
|
||||||
|
|
||||||
|
## 주요 대상 파일
|
||||||
|
|
||||||
|
### 생성
|
||||||
|
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/Models/CreatorChannelFanTalkTabResponse.swift`
|
||||||
|
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/Repository/CreatorChannelFanTalkApi.swift`
|
||||||
|
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/Repository/CreatorChannelFanTalkRepository.swift`
|
||||||
|
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/CreatorChannelFanTalkViewModel.swift`
|
||||||
|
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/CreatorChannelFanTalkTabView.swift`
|
||||||
|
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkCountBar.swift`
|
||||||
|
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkListItem.swift`
|
||||||
|
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkActionPopup.swift`
|
||||||
|
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkEmptyView.swift`
|
||||||
|
- `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingIconButton.swift`
|
||||||
|
|
||||||
|
### 수정
|
||||||
|
- `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
|
||||||
|
- `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingActionMenu.swift`
|
||||||
|
- `SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeResponse.swift`
|
||||||
|
- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelFanTalkSection.swift`
|
||||||
|
- `SodaLive/Sources/I18n/I18n.swift`
|
||||||
|
- 필요 시 `SodaLive.xcodeproj/project.pbxproj`
|
||||||
|
|
||||||
|
## TASK 체크리스트
|
||||||
|
|
||||||
|
### Phase 1: 문서와 API 모델
|
||||||
|
|
||||||
|
- [x] **Task 1.1: PRD와 계획 문서 생성**
|
||||||
|
- 대상 파일:
|
||||||
|
- 생성: `docs/20260704_크리에이터_채널_팬Talk_탭/prd.md`
|
||||||
|
- 생성: `docs/20260704_크리에이터_채널_팬Talk_탭/plan-task.md`
|
||||||
|
- 검증 기준:
|
||||||
|
- 실행 명령: `rg "290:9139|290:9145|290:9147|666:36114|290:9001|CreatorChannelFanTalkTabResponse|/api/v2/creator-channels/.*/fan-talks|ic_new_more" docs/20260704_크리에이터_채널_팬Talk_탭`
|
||||||
|
- 기대 결과: Figma URL, 응답 모델, API path, 액션 요구사항이 문서에 기록된다.
|
||||||
|
|
||||||
|
- [x] **Task 1.2: 팬Talk 탭 Response model 생성**
|
||||||
|
- 대상 파일:
|
||||||
|
- 생성: `SodaLive/Sources/V2/CreatorChannel/FanTalk/Models/CreatorChannelFanTalkTabResponse.swift`
|
||||||
|
- 수정: `SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeResponse.swift`
|
||||||
|
- 수정: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelFanTalkSection.swift`
|
||||||
|
- 작업 내용:
|
||||||
|
- `CreatorChannelFanTalkTabResponse`, `CreatorChannelFanTalkItemResponse`, `CreatorChannelFanTalkReplyResponse`를 `Decodable`로 생성한다.
|
||||||
|
- 홈 탭 기존 `CreatorChannelFanTalkResponse`와 충돌하지 않도록 홈 탭 타입명을 변경하거나 신규 타입명을 다르게 둔다.
|
||||||
|
- `Identifiable`은 `fanTalkId`를 `id`로 사용한다.
|
||||||
|
- 검증 기준:
|
||||||
|
- 실행 명령: `rg "struct CreatorChannelFanTalkTabResponse|fanTalkCount|fanTalks|hasNext|struct CreatorChannelFanTalkItemResponse|writerId|creatorReplies|struct CreatorChannelFanTalkReplyResponse" SodaLive/Sources/V2/CreatorChannel/FanTalk/Models/CreatorChannelFanTalkTabResponse.swift`
|
||||||
|
|
||||||
|
### Phase 2: API, Repository, ViewModel
|
||||||
|
|
||||||
|
- [x] **Task 2.1: 팬Talk 탭 API endpoint 생성**
|
||||||
|
- 대상 파일:
|
||||||
|
- 생성: `SodaLive/Sources/V2/CreatorChannel/FanTalk/Repository/CreatorChannelFanTalkApi.swift`
|
||||||
|
- 작업 내용:
|
||||||
|
- `CreatorChannelFanTalkApi.getFanTalks(creatorId:page:size:)`를 만든다.
|
||||||
|
- path는 `/api/v2/creator-channels/{creatorId}/fan-talks`로 구성한다.
|
||||||
|
- query parameter는 `page`, `size`만 보낸다.
|
||||||
|
- 검증 기준:
|
||||||
|
- 실행 명령: `rg "CreatorChannelFanTalkApi|getFanTalks|/api/v2/creator-channels/|/fan-talks|page|size|URLEncoding.queryString" SodaLive/Sources/V2/CreatorChannel/FanTalk/Repository/CreatorChannelFanTalkApi.swift`
|
||||||
|
|
||||||
|
- [x] **Task 2.2: 팬Talk 탭 Repository 생성**
|
||||||
|
- 대상 파일:
|
||||||
|
- 생성: `SodaLive/Sources/V2/CreatorChannel/FanTalk/Repository/CreatorChannelFanTalkRepository.swift`
|
||||||
|
- 작업 내용:
|
||||||
|
- `MoyaProvider<CreatorChannelFanTalkApi>`를 사용하는 `getFanTalks(...)` publisher를 제공한다.
|
||||||
|
- 검증 기준:
|
||||||
|
- 실행 명령: `rg "CreatorChannelFanTalkRepository|MoyaProvider<CreatorChannelFanTalkApi>|AnyPublisher<Response, MoyaError>|getFanTalks" SodaLive/Sources/V2/CreatorChannel/FanTalk/Repository/CreatorChannelFanTalkRepository.swift`
|
||||||
|
|
||||||
|
- [x] **Task 2.3: 팬Talk 탭 ViewModel 생성**
|
||||||
|
- 대상 파일:
|
||||||
|
- 생성: `SodaLive/Sources/V2/CreatorChannel/FanTalk/CreatorChannelFanTalkViewModel.swift`
|
||||||
|
- 작업 내용:
|
||||||
|
- 상태는 loading, loadingNextPage, response, fanTalks, page, size, hasNext, hasLoaded, error toast, 신고/삭제 dialog 상태를 둔다.
|
||||||
|
- `fetchFirstPage(creatorId:)`는 `page=0`, `size=20`으로 조회한다.
|
||||||
|
- `fetchNextPageIfNeeded(creatorId:currentItem:)`는 마지막 item 기준으로 다음 페이지를 조회한다.
|
||||||
|
- 첫 페이지는 목록 교체, 다음 페이지는 append한다.
|
||||||
|
- `report(reason:)`는 `ReportRequest(type: .CHEERS, cheersId: selectedFanTalkId, reason: reason)`를 호출한다.
|
||||||
|
- `deleteSelectedFanTalk(creatorId:)`는 `ExplorerRepository.modifyCheers(..., isActive: false)`를 호출하고 성공 후 첫 페이지를 다시 조회한다.
|
||||||
|
- 최신 요청만 반영하도록 `latestRequestId` 패턴을 사용한다.
|
||||||
|
- 검증 기준:
|
||||||
|
- 실행 명령: `rg "CreatorChannelFanTalkViewModel|fetchFirstPage|fetchNextPageIfNeeded|isLoadingNextPage|latestRequestId|report\\(|deleteSelectedFanTalk|ReportType\\.CHEERS|modifyCheers|hasNext" SodaLive/Sources/V2/CreatorChannel/FanTalk/CreatorChannelFanTalkViewModel.swift`
|
||||||
|
|
||||||
|
### Phase 3: Figma 섹션별 UI
|
||||||
|
|
||||||
|
- [x] **Task 3.1: 팬Talk count bar 구현**
|
||||||
|
- 대상 파일:
|
||||||
|
- 생성: `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkCountBar.swift`
|
||||||
|
- 작업 내용:
|
||||||
|
- Figma sort-bar 기준으로 높이 52pt, 좌측 `전체 {fanTalkCount}`만 표시한다.
|
||||||
|
- 정렬 버튼/아이콘/action은 포함하지 않는다.
|
||||||
|
- 검증 기준:
|
||||||
|
- 실행 명령: `rg "CreatorChannelFanTalkCountBar|fanTalkCount|전체|frame\\(height: 52\\)|ic_new_sort|selectedSort" SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkCountBar.swift`
|
||||||
|
- 기대 결과: count bar 구현이 확인되고 정렬 관련 문자열은 없어야 한다.
|
||||||
|
|
||||||
|
- [x] **Task 3.2: 팬Talk item 구현**
|
||||||
|
- 대상 파일:
|
||||||
|
- 생성: `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkListItem.swift`
|
||||||
|
- 작업 내용:
|
||||||
|
- 작성자 프로필 42pt, 닉네임/상대 시간, 본문 50pt inset, 하단 separator를 구현한다.
|
||||||
|
- 답글이 있으면 좌측 라인과 답글 카드를 표시한다.
|
||||||
|
- `isMine || isOwnCreatorChannel`이면 `ic_new_more` 버튼을 표시한다.
|
||||||
|
- 그 외에는 `신고` 텍스트 버튼을 표시한다.
|
||||||
|
- 프로필 이미지와 답글 프로필 이미지는 Kingfisher 기반 기존 컴포넌트를 사용한다.
|
||||||
|
- 검증 기준:
|
||||||
|
- 실행 명령: `rg "CreatorChannelFanTalkListItem|writerProfileImageUrl|writerNickname|createdAtUtc|creatorReplies|ic_new_more|신고|Color\\.gray800|Color\\.gray900|42|20" SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkListItem.swift`
|
||||||
|
|
||||||
|
- [x] **Task 3.3: 더보기 anchored context popup 구현**
|
||||||
|
- 대상 파일:
|
||||||
|
- 생성: `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkActionPopup.swift`
|
||||||
|
- 작업 내용:
|
||||||
|
- Figma `666:36114` 기준으로 border `Color.gray700`, background `Color.gray900`, radius 14pt를 사용한다.
|
||||||
|
- 내가 쓴 글이면 `수정하기`, `삭제하기`를 표시한다.
|
||||||
|
- 내가 쓴 글이 아니지만 내 채널이면 `삭제하기`만 표시한다.
|
||||||
|
- tap action은 부모에서 주입한다.
|
||||||
|
- 검증 기준:
|
||||||
|
- 실행 명령: `rg "CreatorChannelFanTalkActionPopup|수정하기|삭제하기|Color\\.gray700|Color\\.gray900|cornerRadius\\(14\\)|onTapEdit|onTapDelete" SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkActionPopup.swift`
|
||||||
|
|
||||||
|
- [x] **Task 3.4: Empty view 구현**
|
||||||
|
- 대상 파일:
|
||||||
|
- 생성: `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkEmptyView.swift`
|
||||||
|
- 작업 내용:
|
||||||
|
- Figma `290:9001` 기준으로 `아직 응원이 없습니다.\n처음으로 크리에이터를 응원해 보세요!` 문구를 표시한다.
|
||||||
|
- plus 아이콘과 `응원 남기기` 텍스트가 있는 capsule button을 표시한다.
|
||||||
|
- 내 채널이면 button을 숨긴다.
|
||||||
|
- tap action은 부모에서 주입한다.
|
||||||
|
- 검증 기준:
|
||||||
|
- 실행 명령: `rg "CreatorChannelFanTalkEmptyView|아직 응원이 없습니다|처음으로 크리에이터를 응원해 보세요|응원 남기기|showsWriteButton|onTapWrite" SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkEmptyView.swift`
|
||||||
|
|
||||||
|
- [x] **Task 3.5: 공용 icon floating button 추출**
|
||||||
|
- 대상 파일:
|
||||||
|
- 생성: `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingIconButton.swift`
|
||||||
|
- 수정: `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingActionMenu.swift`
|
||||||
|
- 작업 내용:
|
||||||
|
- `CreatorChannelFloatingActionMenu` 내부 private `floatingButton`과 같은 66pt 원형 버튼, 38pt 아이콘, background color, plain button style을 공용 컴포넌트로 분리한다.
|
||||||
|
- 공용 컴포넌트는 `imageName`, `backgroundColor`, `accessibilityLabel`, `action`을 받는다.
|
||||||
|
- 기존 `CreatorChannelFloatingActionMenu`는 private `floatingButton` 구현 대신 공용 컴포넌트를 사용하도록 변경한다.
|
||||||
|
- 팬Talk 우측하단 글쓰기 plus button은 이 공용 컴포넌트에 `imageName: "ic_plus_no_bg"`, `backgroundColor: Color.soda400`를 전달해 사용한다.
|
||||||
|
- 글쓰기 plus button은 텍스트를 표시하지 않는다.
|
||||||
|
- 검증 기준:
|
||||||
|
- 실행 명령: `rg "struct CreatorChannelFloatingIconButton|imageName|backgroundColor|accessibilityLabel|frame\\(width: 38, height: 38\\)|frame\\(width: 66, height: 66\\)|clipShape\\(Circle\\(\\)\\)" SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingIconButton.swift`
|
||||||
|
- 실행 명령: `rg "CreatorChannelFloatingIconButton|ic_plus_no_bg|ic_new_upload_community_post|ic_new_upload_audio|ic_new_create_live|ic_new_x_black" SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingActionMenu.swift SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
|
||||||
|
- 실행 명령: `rg "Text\\(" SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingIconButton.swift`
|
||||||
|
- 기대 결과: 공용 icon-only floating button과 기존 floating action menu 재사용이 확인되고, 공용 컴포넌트에는 텍스트가 없어야 한다.
|
||||||
|
|
||||||
|
- [x] **Task 3.6: 팬Talk 탭 본문 구현**
|
||||||
|
- 대상 파일:
|
||||||
|
- 생성: `SodaLive/Sources/V2/CreatorChannel/FanTalk/CreatorChannelFanTalkTabView.swift`
|
||||||
|
- 작업 내용:
|
||||||
|
- count bar, empty view, fanTalk list를 배치한다.
|
||||||
|
- 첫 진입 시 `fetchFirstPage(creatorId:)`를 호출한다.
|
||||||
|
- 마지막 fanTalk item 노출 시 `fetchNextPageIfNeeded`를 호출한다.
|
||||||
|
- `fanTalkCount == 0`이면 `CreatorChannelFanTalkEmptyView`를 표시한다.
|
||||||
|
- 내 채널인 경우 empty view 내부 글쓰기 버튼을 숨긴다.
|
||||||
|
- `CheersReportDialogView`와 삭제 confirm dialog를 표시한다.
|
||||||
|
- 검증 기준:
|
||||||
|
- 실행 명령: `rg "CreatorChannelFanTalkTabView|CreatorChannelFanTalkCountBar|CreatorChannelFanTalkEmptyView|CreatorChannelFanTalkListItem|fetchFirstPage|fetchNextPageIfNeeded|CheersReportDialogView|SodaDialog|onTapWrite" SodaLive/Sources/V2/CreatorChannel/FanTalk/CreatorChannelFanTalkTabView.swift`
|
||||||
|
|
||||||
|
### Phase 4: CreatorChannelView 연결
|
||||||
|
|
||||||
|
- [x] **Task 4.1: 팬Talk 탭 콘텐츠 연결**
|
||||||
|
- 대상 파일:
|
||||||
|
- 수정: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
|
||||||
|
- 작업 내용:
|
||||||
|
- `@StateObject`로 팬Talk 탭 ViewModel을 보유한다.
|
||||||
|
- `selectedTabContent`에서 `.fanTalk`일 때 `CreatorChannelFanTalkTabView`를 표시한다.
|
||||||
|
- 우측하단 글쓰기 floating button은 `selectedTab == .fanTalk`, 내 채널이 아니고 `fanTalkCount > 0`일 때만 `CreatorChannelFloatingIconButton`으로 표시한다.
|
||||||
|
- 글쓰기 action은 신규 글쓰기 페이지 구현 전까지 no-op placeholder로 둔다.
|
||||||
|
- 검증 기준:
|
||||||
|
- 실행 명령: `rg "CreatorChannelFanTalkViewModel|CreatorChannelFanTalkTabView|viewModel\\.selectedTab == \\.fanTalk|CreatorChannelFloatingIconButton|showFanTalkWrite|fanTalkCount|ic_plus_no_bg" SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
|
||||||
|
|
||||||
|
### Phase 5: 프로젝트 등록과 검증
|
||||||
|
|
||||||
|
- [x] **Task 5.1: 프로젝트 파일 등록과 정적 검증**
|
||||||
|
- 대상 파일:
|
||||||
|
- 확인/수정: `SodaLive.xcodeproj/project.pbxproj`
|
||||||
|
- 검증 기준:
|
||||||
|
- 실행 명령: `git diff --check -- docs/20260704_크리에이터_채널_팬Talk_탭 SodaLive/Sources/V2/CreatorChannel SodaLive/Sources/I18n/I18n.swift SodaLive.xcodeproj/project.pbxproj`
|
||||||
|
- 기대 결과: 공백 오류 없이 성공한다.
|
||||||
|
- 실행 명령: `plutil -lint SodaLive.xcodeproj/project.pbxproj`
|
||||||
|
- 기대 결과: `OK` 출력.
|
||||||
|
|
||||||
|
- [x] **Task 5.2: 빌드 검증**
|
||||||
|
- 대상 파일:
|
||||||
|
- 확인: `SodaLive.xcworkspace`
|
||||||
|
- 확인: `SodaLive.xcodeproj`
|
||||||
|
- 검증 기준:
|
||||||
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
|
||||||
|
- 기대 결과: `BUILD SUCCEEDED`.
|
||||||
|
- workspace 인식 실패 등 환경 문제가 있으면 `xcodebuild -project "SodaLive.xcodeproj" -scheme "SodaLive-dev" -configuration Debug build`를 대체 검증으로 실행하고 실패 원인을 검증 기록에 남긴다.
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
|
||||||
|
- 2026-07-04: Figma `get_design_context`와 `get_screenshot`으로 `290:9139`, `290:9145`, `290:9147`, `666:36114`, `290:9001`을 확인했다.
|
||||||
|
- 2026-07-04: `rg`와 `sed`로 기존 `CreatorChannelView`, 후원 탭 구현, `UserProfileFanTalkViewModel`, `UserProfileFanTalkCheersItemView`, `UserProfileView` 신고/삭제 흐름을 확인했다.
|
||||||
|
- 2026-07-04: 사용자 피드백에 따라 우측하단 plus button은 `CreatorChannelFloatingActionMenu`의 private `floatingButton`과 동일 UI로 판단했다. 중복 컴포넌트를 만들지 않고 `CreatorChannelFloatingIconButton` 공용 컴포넌트로 추출해 `CreatorChannelFloatingActionMenu`와 팬Talk 글쓰기 floating button에서 재사용하는 방향으로 계획을 보완했다.
|
||||||
|
- 2026-07-04: `xcodebuild -workspace "SodaLive.xcworkspace" -list`로 `SodaLive`, `SodaLive-dev` 앱 스킴은 확인했지만 테스트 action 구성은 확인되지 않았다.
|
||||||
|
- 2026-07-04: `rg` 검증으로 팬Talk 탭 response model, API endpoint, Repository, ViewModel, count bar, list item, action popup, empty view, tab view, `CreatorChannelView` 연결 문자열을 확인했다.
|
||||||
|
- 2026-07-04: `plutil -lint SodaLive.xcodeproj/project.pbxproj` 결과 `OK`를 확인했다.
|
||||||
|
- 2026-07-04: `git diff --check -- docs/20260704_크리에이터_채널_팬Talk_탭 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` 결과 `BUILD SUCCEEDED`를 확인했다.
|
||||||
|
- 2026-07-04: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test`는 `Scheme SodaLive-dev is not currently configured for the test action.`으로 실패했다. 현재 범위에서는 새 테스트 타깃을 추가하지 않고 빌드/정적 검증으로 대체했다.
|
||||||
182
docs/20260704_크리에이터_채널_팬Talk_탭/prd.md
Normal file
182
docs/20260704_크리에이터_채널_팬Talk_탭/prd.md
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
# PRD: 크리에이터 채널 팬Talk 탭
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
크리에이터 채널 공통 shell의 `팬Talk` 탭에서 팬이 남긴 응원글 목록과 크리에이터 답글을 제공한다. 상단 title bar, header, sticky tab-bar는 기존 크리에이터 채널 홈/라이브/오디오/시리즈/후원 구현을 재사용하고, tab-bar 아래 콘텐츠만 팬Talk 탭 전용 API 응답으로 구성한다.
|
||||||
|
|
||||||
|
API는 `GET /api/v2/creator-channels/{creatorId}/fan-talks`를 사용한다. `creatorId`는 path variable이며, query parameter는 `page`, `size`를 사용한다. 기본값은 `page=0`, `size=20`이다.
|
||||||
|
|
||||||
|
Figma 참조:
|
||||||
|
- 전체 화면: `290:9139`, `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-9139&m=dev`
|
||||||
|
- 아이템 뷰(답글 없음): `290:9145`, `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-9145&m=dev`
|
||||||
|
- 아이템 뷰(답글 있음): `290:9147`, `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-9147&m=dev`
|
||||||
|
- 더보기 anchored context popup: `666:36114`, `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=666-36114&m=dev`
|
||||||
|
- Empty: `290:9001`, `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-9001&m=dev`
|
||||||
|
|
||||||
|
## 2. Problem
|
||||||
|
- 현재 `CreatorChannelTab.fanTalk`은 placeholder로 표시되어 팬Talk 목록을 확인할 수 없다.
|
||||||
|
- 기존 홈 탭에는 최신 팬Talk 요약만 있고, 팬Talk 탭 전용 목록 API, 페이징, 답글 표시, 신고/삭제 액션이 없다.
|
||||||
|
- 기존 프로필 FanTalk 구현은 구 API와 구 UI 구조를 사용하므로 V2 크리에이터 채널 탭에 그대로 붙이기 어렵다.
|
||||||
|
|
||||||
|
## 3. Goals
|
||||||
|
- `CreatorChannelTab.fanTalk` 선택 시 팬Talk 탭 API를 호출하고 응답 데이터로 화면을 구성한다.
|
||||||
|
- 기본 query는 `page=0`, `size=20`이다.
|
||||||
|
- sort-bar에는 정렬 UI 없이 `전체 {fanTalkCount}`만 표시한다.
|
||||||
|
- 팬Talk item은 Figma `290:9145`, `290:9147` 기준으로 작성자 프로필, 닉네임, 상대 시간, 본문, 답글 목록을 표시한다.
|
||||||
|
- 답글이 있으면 Figma처럼 좌측 라인과 답글 카드 영역을 표시한다.
|
||||||
|
- 다른 유저의 글에는 `신고` 버튼만 표시하고, 기존 `UserProfileView`의 응원글 신고 기능과 동일하게 `CheersReportDialogView` 및 `ReportType.CHEERS` 흐름을 사용한다.
|
||||||
|
- 내가 쓴 글 또는 내 채널의 타인 글에는 우측 액션으로 `ic_new_more` 더보기 버튼을 표시한다.
|
||||||
|
- 내가 쓴 글의 더보기 메뉴에는 `수정하기`, `삭제하기`를 표시한다.
|
||||||
|
- 내가 쓴 글이 아니지만 내 채널인 경우 더보기 메뉴에는 `삭제하기`만 표시한다.
|
||||||
|
- `삭제하기`는 기존 FanTalk에서 사용하던 삭제 API와 동일하게 `ExplorerRepository.modifyCheers(..., isActive: false)`를 사용한다.
|
||||||
|
- `fanTalkCount == 0`이면 Figma empty view(`290:9001`)를 표시한다.
|
||||||
|
- 내 채널인 경우 우측하단 글쓰기 floating button과 empty view 내부 `응원 남기기` 버튼을 모두 표시하지 않는다.
|
||||||
|
|
||||||
|
## 4. Non-Goals
|
||||||
|
- 크리에이터 채널 공통 shell, header, sticky tab-bar 동작을 다시 설계하지 않는다.
|
||||||
|
- 신규 글쓰기/수정 페이지를 이번 작업에서 구현하지 않는다.
|
||||||
|
- 팬Talk 작성/수정 mutation API를 새로 만들지 않는다.
|
||||||
|
- 신고 API나 신고 dialog를 새로 만들지 않는다.
|
||||||
|
- 삭제 API를 새로 만들지 않고 기존 FanTalk 삭제 흐름을 재사용한다.
|
||||||
|
- Figma localhost asset URL을 앱 코드에 직접 사용하지 않는다.
|
||||||
|
- `Pods/**`, `generated/**`, `build/**`는 수정하지 않는다.
|
||||||
|
|
||||||
|
## 5. Core Requirements
|
||||||
|
|
||||||
|
### 5.1 API
|
||||||
|
- Method: `GET`
|
||||||
|
- Path: `/api/v2/creator-channels/{creatorId}/fan-talks`
|
||||||
|
- Path parameter:
|
||||||
|
- `creatorId`
|
||||||
|
- Query parameters:
|
||||||
|
- `page`: 기본값 `0`
|
||||||
|
- `size`: 기본값 `20`
|
||||||
|
- 응답 래퍼는 기존 패턴대로 `ApiResponse<CreatorChannelFanTalkTabResponse>`로 디코딩한다.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
data class CreatorChannelFanTalkTabResponse(
|
||||||
|
val fanTalkCount: Int,
|
||||||
|
val fanTalks: List<CreatorChannelFanTalkResponse>,
|
||||||
|
val page: Int,
|
||||||
|
val size: Int,
|
||||||
|
@JsonProperty("hasNext")
|
||||||
|
val hasNext: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreatorChannelFanTalkResponse(
|
||||||
|
val fanTalkId: Long,
|
||||||
|
val writerId: Long,
|
||||||
|
val writerNickname: String,
|
||||||
|
val writerProfileImageUrl: String,
|
||||||
|
val content: String,
|
||||||
|
val createdAtUtc: String,
|
||||||
|
val creatorReplies: List<CreatorChannelFanTalkReplyResponse>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreatorChannelFanTalkReplyResponse(
|
||||||
|
val fanTalkId: Long,
|
||||||
|
val writerId: Long,
|
||||||
|
val writerNickname: String,
|
||||||
|
val writerProfileImageUrl: String,
|
||||||
|
val content: String,
|
||||||
|
val createdAtUtc: String
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Kotlin `Long`은 기존 V2 모델 관례대로 Swift `Int`로 선언한다.
|
||||||
|
- `CreatorChannelFanTalkResponse`는 홈 탭 모델과 이름이 충돌하므로 홈 탭 모델명을 변경하거나 팬Talk 탭 모델명을 분리해 컴파일 충돌을 피한다.
|
||||||
|
|
||||||
|
### 5.2 Count bar
|
||||||
|
- 팬Talk 탭 sort-bar는 정렬이 없고 전체 개수만 포함한다.
|
||||||
|
- 좌측에 `전체`과 `fanTalkCount`를 표시한다.
|
||||||
|
- 우측 정렬 버튼, 정렬 아이콘, 정렬 context popup, BottomSheet는 표시하지 않는다.
|
||||||
|
- 후원 탭의 `CreatorChannelDonationCountBar`와 같은 시각 규칙을 사용하되 FanTalk 전용 이름으로 분리하거나 공용화한다.
|
||||||
|
|
||||||
|
### 5.3 FanTalk item
|
||||||
|
- item 전체 배경은 `Color.black`, 하단 separator는 `Color.gray800` 기준을 사용한다.
|
||||||
|
- item padding은 Figma 기준 14pt를 사용한다.
|
||||||
|
- 작성자 프로필 이미지는 42pt 원형으로 표시한다.
|
||||||
|
- 작성자 닉네임은 14pt medium, 작성 시간은 14pt regular와 `Color.gray500`으로 표시한다.
|
||||||
|
- 본문은 16pt regular, `Color.white`, 좌측 50pt inset으로 표시한다.
|
||||||
|
- 본문과 답글은 서버 응답의 문자열을 그대로 표시한다.
|
||||||
|
- 작성 시간은 기존 프로젝트의 상대 시간 표시 helper를 우선 재사용한다.
|
||||||
|
- 이미지 로딩은 기존 V2 관례대로 `DownsampledKFImage` 또는 Kingfisher 기반 컴포넌트를 사용한다.
|
||||||
|
|
||||||
|
### 5.4 Creator replies
|
||||||
|
- `creatorReplies`가 비어 있으면 답글 영역을 표시하지 않는다.
|
||||||
|
- `creatorReplies`가 있으면 각 답글을 Figma `290:9147` 기준으로 표시한다.
|
||||||
|
- 답글 카드 배경은 `Color.gray900`, radius는 14pt, padding은 14pt를 사용한다.
|
||||||
|
- 답글 작성자 프로필은 20pt 원형으로 표시하고 닉네임과 상대 시간을 한 줄에 표시한다.
|
||||||
|
- 답글 본문은 16pt regular, `Color.white`로 표시한다.
|
||||||
|
- 답글 영역 왼쪽에는 Figma처럼 세로 라인을 표시한다.
|
||||||
|
|
||||||
|
### 5.5 Item action rules
|
||||||
|
- 현재 로그인 사용자의 id는 기존 관례대로 `UserDefaults.int(forKey: .userId)`를 사용한다.
|
||||||
|
- `isMine = fanTalk.writerId == currentUserId`.
|
||||||
|
- `isOwnCreatorChannel`은 `CreatorChannelView`에서 계산한 값을 주입한다.
|
||||||
|
- `isMine || isOwnCreatorChannel`이면 우측에 `ic_new_more` 더보기 버튼을 표시한다.
|
||||||
|
- `!isMine && !isOwnCreatorChannel`이면 우측에 `신고` 버튼만 표시한다.
|
||||||
|
- 더보기 버튼을 터치하면 Figma `666:36114` 기준 anchored context popup을 표시한다.
|
||||||
|
- 내가 쓴 글이면 `수정하기`, `삭제하기`를 표시한다.
|
||||||
|
- 내가 쓴 글이 아니지만 내 채널이면 `삭제하기`만 표시한다.
|
||||||
|
- `수정하기`와 글쓰기 floating button은 후속 신규 글쓰기 페이지 구현 전까지 비어 있는 placeholder action으로 두되, 코드상 TODO가 드러나게 유지한다.
|
||||||
|
- `삭제하기`는 확인 dialog 후 기존 삭제 API를 호출한다.
|
||||||
|
|
||||||
|
### 5.6 Report and delete
|
||||||
|
- 신고 버튼을 터치하면 기존 크리에이터 페이지의 응원글 신고와 동일하게 `CheersReportDialogView`를 표시한다.
|
||||||
|
- 신고 confirm 시 `ReportRequest(type: .CHEERS, cheersId: fanTalkId, reason: reason)` 흐름을 사용한다.
|
||||||
|
- 삭제 confirm dialog는 기존 `I18n.MemberChannel.cheersDeleteTitle`, `I18n.Common.confirmDeleteQuestion`, `I18n.Common.delete`, `I18n.Common.cancel`을 재사용한다.
|
||||||
|
- 삭제 API는 기존 FanTalk 삭제 흐름과 동일하게 `ExplorerRepository.modifyCheers(cheersId: fanTalkId, content: nil, isActive: false)`를 호출한다.
|
||||||
|
- 삭제 성공 후 팬Talk 탭 첫 페이지를 다시 조회한다.
|
||||||
|
|
||||||
|
### 5.7 Empty and writing button
|
||||||
|
- `fanTalkCount == 0`이면 Figma `290:9001` 기준 empty view를 표시한다.
|
||||||
|
- Empty 문구는 `아직 응원이 없습니다.\n처음으로 크리에이터를 응원해 보세요!`를 표시한다.
|
||||||
|
- Empty view에는 `+` 아이콘과 `응원 남기기` 텍스트가 있는 capsule button을 표시한다.
|
||||||
|
- 단, 내 채널인 경우 empty view 내부 `응원 남기기` 버튼을 표시하지 않는다.
|
||||||
|
- `fanTalkCount > 0`이면 홈 탭 plus button 위치에 글쓰기 floating button을 표시한다.
|
||||||
|
- 단, 내 채널인 경우 글쓰기 floating button을 표시하지 않는다.
|
||||||
|
- 글쓰기 floating button은 `CreatorChannelFloatingActionMenu` 내부 `floatingButton`과 동일한 66pt 원형 버튼, 38pt 아이콘, `Color.soda400` 배경을 사용한다.
|
||||||
|
- 동일 UI를 중복 구현하지 않기 위해 `SodaLive/Sources/V2/CreatorChannel/Components/**` 아래에 공용 icon floating button을 분리하고, `CreatorChannelFloatingActionMenu`와 팬Talk 글쓰기 floating button이 이를 재사용한다.
|
||||||
|
- 글쓰기 floating button에는 plus 아이콘만 표시하고 텍스트는 표시하지 않는다.
|
||||||
|
- 글쓰기 버튼 tap action은 신규 글쓰기 페이지 구현 후 연결할 수 있도록 부모 closure로 분리한다.
|
||||||
|
|
||||||
|
### 5.8 Pagination
|
||||||
|
- 첫 진입 시 `page=0`, `size=20`으로 조회한다.
|
||||||
|
- 다음 페이지 로딩 중 중복 호출을 막는다.
|
||||||
|
- 마지막 item이 화면에 나타났고 `hasNext == true`이면 `page + 1`을 조회한다.
|
||||||
|
- 다음 페이지 성공 시 기존 `fanTalks` 뒤에 append한다.
|
||||||
|
- 첫 페이지 재조회 시 기존 목록을 교체한다.
|
||||||
|
|
||||||
|
## 6. Success Criteria
|
||||||
|
- 팬Talk 탭 진입 시 `GET /api/v2/creator-channels/{creatorId}/fan-talks?page=0&size=20`가 호출된다.
|
||||||
|
- count bar에는 `전체 {fanTalkCount}`만 표시되고 정렬 버튼/아이콘/popup은 표시되지 않는다.
|
||||||
|
- 답글이 없는 item은 Figma `290:9145`와 같은 정보 구조로 표시된다.
|
||||||
|
- 답글이 있는 item은 Figma `290:9147`처럼 좌측 라인과 답글 카드가 표시된다.
|
||||||
|
- 타인의 글이면서 내 채널이 아니면 `신고` 버튼만 표시되고 신고 confirm 시 기존 응원글 신고 API가 호출된다.
|
||||||
|
- 내가 쓴 글은 `ic_new_more` 버튼과 `수정하기`, `삭제하기` 메뉴를 표시한다.
|
||||||
|
- 내가 쓴 글이 아니지만 내 채널이면 `ic_new_more` 버튼과 `삭제하기` 메뉴만 표시한다.
|
||||||
|
- 삭제 confirm 시 기존 FanTalk 삭제 API가 호출되고 성공 후 첫 페이지가 갱신된다.
|
||||||
|
- `fanTalkCount == 0`이면 Figma `290:9001` empty view가 표시된다.
|
||||||
|
- 내 채널인 경우 empty view 내부 `응원 남기기` 버튼과 우측하단 글쓰기 floating button이 모두 표시되지 않는다.
|
||||||
|
- `fanTalkCount > 0`이고 내 채널이 아니면 우측하단 글쓰기 floating button이 표시된다.
|
||||||
|
- 글쓰기/수정하기 tap은 신규 글쓰기 페이지 구현 전까지 앱 crash 없이 no-op placeholder로 동작한다.
|
||||||
|
|
||||||
|
## 7. Technical Constraints
|
||||||
|
- 기능 변경은 `SodaLive/Sources/V2/CreatorChannel/**` 하위에서 해결한다.
|
||||||
|
- 팬Talk 탭 전용 View, ViewModel, Repository, API, 모델은 `SodaLive/Sources/V2/CreatorChannel/FanTalk/**` 아래에 둔다.
|
||||||
|
- 우측하단 icon floating button은 팬Talk 전용 컴포넌트로 중복 생성하지 않고 `CreatorChannelFloatingActionMenu`의 private `floatingButton` 패턴을 공용 컴포넌트로 추출해 재사용한다.
|
||||||
|
- 삭제 API 재사용을 위해 기존 `ExplorerRepository.modifyCheers`를 호출할 수 있다.
|
||||||
|
- 신고 기능 재사용을 위해 기존 `ReportRepository`, `ReportRequest`, `CheersReportDialogView`를 사용한다.
|
||||||
|
- 신규 문구가 필요하면 `SodaLive/Sources/I18n/I18n.swift`에 ko/en/ja를 추가한다.
|
||||||
|
- 프로젝트 설정 변경은 신규 Swift 파일 target 등록이 필요한 경우에만 수행한다.
|
||||||
|
|
||||||
|
## 8. Assumptions
|
||||||
|
- 신규 글쓰기/수정 페이지는 후속 작업에서 구현하며, 이번 작업에서는 버튼 표시와 action closure만 준비한다.
|
||||||
|
- 신규 API의 `fanTalkId`는 기존 신고/삭제 API가 요구하는 `cheersId`와 동일한 식별자로 사용할 수 있다.
|
||||||
|
|
||||||
|
## 9. Open Questions
|
||||||
|
- 해당 없음
|
||||||
|
|
||||||
|
## 10. Verification Notes
|
||||||
|
- 2026-07-04: Figma `get_design_context`와 `get_screenshot`으로 `290:9139`, `290:9145`, `290:9147`, `666:36114`, `290:9001`을 확인했다.
|
||||||
|
- 2026-07-04: `CreatorChannelView`, 후원 탭 구현, 기존 `UserProfileFanTalkViewModel`, `UserProfileFanTalkCheersItemView`, `UserProfileView`의 신고/삭제 흐름을 확인했다.
|
||||||
Reference in New Issue
Block a user