feat(creator): 팬Talk 쓰기 화면을 연결한다

This commit is contained in:
Yu Sung
2026-07-06 22:54:20 +09:00
parent d129583d3b
commit 9c4c304fc6
11 changed files with 716 additions and 10 deletions

View File

@@ -95,6 +95,9 @@
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 */; }; 3BEBC2613F5CCEC2118B497B /* CreatorChannelFanTalkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C5D06E4F54C0ED7B7BD462 /* CreatorChannelFanTalkViewModel.swift */; };
3BEBC2623F5CCEC2118B497B /* CreatorChannelFanTalkWriteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C5D06F4F54C0ED7B7BD462 /* CreatorChannelFanTalkWriteViewModel.swift */; };
3BEBC2633F5CCEC2118B497B /* CreatorChannelFanTalkWriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C5D0704F54C0ED7B7BD462 /* CreatorChannelFanTalkWriteView.swift */; };
3BEBC2643F5CCEC2118B497B /* SodaV2ActionModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C5D0714F54C0ED7B7BD462 /* SodaV2ActionModal.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 */; };
@@ -1675,6 +1678,9 @@
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 */; }; 5AE12C82F7B160F075A5A6A8 /* CreatorChannelFanTalkTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F1C1B0C73E8D92112E47610 /* CreatorChannelFanTalkTabResponse.swift */; };
5AE12C83F7B160F075A5A6A8 /* CreatorChannelFanTalkWriteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C5D06F4F54C0ED7B7BD462 /* CreatorChannelFanTalkWriteViewModel.swift */; };
5AE12C84F7B160F075A5A6A8 /* CreatorChannelFanTalkWriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C5D0704F54C0ED7B7BD462 /* CreatorChannelFanTalkWriteView.swift */; };
5AE12C85F7B160F075A5A6A8 /* SodaV2ActionModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C5D0714F54C0ED7B7BD462 /* SodaV2ActionModal.swift */; };
5B9BFA224A49A5AF0757FDF7 /* CreatorChannelFloatingIconButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40163E3E9DB325EA984D3C4 /* CreatorChannelFloatingIconButton.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 */; };
@@ -1771,6 +1777,9 @@
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 */; }; B0DD3E23BFA368AD7007AF23 /* CreatorChannelFanTalkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C5D06E4F54C0ED7B7BD462 /* CreatorChannelFanTalkViewModel.swift */; };
B0DD3E24BFA368AD7007AF23 /* CreatorChannelFanTalkWriteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C5D06F4F54C0ED7B7BD462 /* CreatorChannelFanTalkWriteViewModel.swift */; };
B0DD3E25BFA368AD7007AF23 /* CreatorChannelFanTalkWriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C5D0704F54C0ED7B7BD462 /* CreatorChannelFanTalkWriteView.swift */; };
B0DD3E26BFA368AD7007AF23 /* SodaV2ActionModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C5D0714F54C0ED7B7BD462 /* SodaV2ActionModal.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 */; };
@@ -1997,6 +2006,9 @@
CC06300A2FF0600000A5BBA3 /* CreatorChannelCommunityEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC06000A2FF0600000A5BBA3 /* CreatorChannelCommunityEmptyView.swift */; }; CC06300A2FF0600000A5BBA3 /* CreatorChannelCommunityEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC06000A2FF0600000A5BBA3 /* CreatorChannelCommunityEmptyView.swift */; };
CD256952CC366FFB67557807 /* CreatorChannelFanTalkApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 002435A7145D59CCCA8F76D1 /* CreatorChannelFanTalkApi.swift */; }; CD256952CC366FFB67557807 /* CreatorChannelFanTalkApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 002435A7145D59CCCA8F76D1 /* CreatorChannelFanTalkApi.swift */; };
CE935959F7A2BF7301F517DB /* CreatorChannelFanTalkTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F1C1B0C73E8D92112E47610 /* CreatorChannelFanTalkTabResponse.swift */; }; CE935959F7A2BF7301F517DB /* CreatorChannelFanTalkTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F1C1B0C73E8D92112E47610 /* CreatorChannelFanTalkTabResponse.swift */; };
CE93595AF7A2BF7301F517DB /* CreatorChannelFanTalkWriteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C5D06F4F54C0ED7B7BD462 /* CreatorChannelFanTalkWriteViewModel.swift */; };
CE93595BF7A2BF7301F517DB /* CreatorChannelFanTalkWriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C5D0704F54C0ED7B7BD462 /* CreatorChannelFanTalkWriteView.swift */; };
CE93595CF7A2BF7301F517DB /* SodaV2ActionModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C5D0714F54C0ED7B7BD462 /* SodaV2ActionModal.swift */; };
CFC28133D4D60FD1E436D97C /* CreatorChannelFanTalkTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C7923ABAD25E9CC4840EC94 /* CreatorChannelFanTalkTabView.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 */; };
@@ -2033,6 +2045,9 @@
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>"; }; 25C5D06E4F54C0ED7B7BD462 /* CreatorChannelFanTalkViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreatorChannelFanTalkViewModel.swift; sourceTree = "<group>"; };
25C5D06F4F54C0ED7B7BD462 /* CreatorChannelFanTalkWriteViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreatorChannelFanTalkWriteViewModel.swift; sourceTree = "<group>"; };
25C5D0704F54C0ED7B7BD462 /* CreatorChannelFanTalkWriteView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreatorChannelFanTalkWriteView.swift; sourceTree = "<group>"; };
25C5D0714F54C0ED7B7BD462 /* SodaV2ActionModal.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SodaV2ActionModal.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>"; };
@@ -5609,6 +5624,7 @@
D8515D4EE6098310CF2C0E21 /* Creator */, D8515D4EE6098310CF2C0E21 /* Creator */,
F7F1421DBAB56E2F6891625E /* DefaultTitleBar.swift */, F7F1421DBAB56E2F6891625E /* DefaultTitleBar.swift */,
DAA81E8424A575A0F8546D31 /* HomeTitleBar.swift */, DAA81E8424A575A0F8546D31 /* HomeTitleBar.swift */,
25C5D0734F54C0ED7B7BD462 /* Modal */,
D16C12DFB83FE164B38CC066 /* SectionTitle.swift */, D16C12DFB83FE164B38CC066 /* SectionTitle.swift */,
0FD267162A809D893E2E3B3F /* Text */, 0FD267162A809D893E2E3B3F /* Text */,
E61608194B084224DB6B78B5 /* TextTabBar.swift */, E61608194B084224DB6B78B5 /* TextTabBar.swift */,
@@ -5617,6 +5633,14 @@
path = Component; path = Component;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
25C5D0734F54C0ED7B7BD462 /* Modal */ = {
isa = PBXGroup;
children = (
25C5D0714F54C0ED7B7BD462 /* SodaV2ActionModal.swift */,
);
path = Modal;
sourceTree = "<group>";
};
C0A702100000000000000000 /* AudioContent */ = { C0A702100000000000000000 /* AudioContent */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -5632,12 +5656,22 @@
09ACFA1137C51118E8147AB4 /* Models */, 09ACFA1137C51118E8147AB4 /* Models */,
67B3FF99E43E66E587F7E07B /* Repository */, 67B3FF99E43E66E587F7E07B /* Repository */,
85110256B2C360449A456D4F /* Components */, 85110256B2C360449A456D4F /* Components */,
25C5D0724F54C0ED7B7BD462 /* Write */,
25C5D06E4F54C0ED7B7BD462 /* CreatorChannelFanTalkViewModel.swift */, 25C5D06E4F54C0ED7B7BD462 /* CreatorChannelFanTalkViewModel.swift */,
9C7923ABAD25E9CC4840EC94 /* CreatorChannelFanTalkTabView.swift */, 9C7923ABAD25E9CC4840EC94 /* CreatorChannelFanTalkTabView.swift */,
); );
path = FanTalk; path = FanTalk;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
25C5D0724F54C0ED7B7BD462 /* Write */ = {
isa = PBXGroup;
children = (
25C5D06F4F54C0ED7B7BD462 /* CreatorChannelFanTalkWriteViewModel.swift */,
25C5D0704F54C0ED7B7BD462 /* CreatorChannelFanTalkWriteView.swift */,
);
path = Write;
sourceTree = "<group>";
};
B068C16B9D7F3F80901622E8 /* Button */ = { B068C16B9D7F3F80901622E8 /* Button */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -6451,6 +6485,9 @@
CC0610082FF0600000A5BBA3 /* CreatorChannelCommunityGridItem.swift in Sources */, CC0610082FF0600000A5BBA3 /* CreatorChannelCommunityGridItem.swift in Sources */,
CC0610092FF0600000A5BBA3 /* CreatorChannelCommunityUploadButton.swift in Sources */, CC0610092FF0600000A5BBA3 /* CreatorChannelCommunityUploadButton.swift in Sources */,
CC06100A2FF0600000A5BBA3 /* CreatorChannelCommunityEmptyView.swift in Sources */, CC06100A2FF0600000A5BBA3 /* CreatorChannelCommunityEmptyView.swift in Sources */,
3BEBC2623F5CCEC2118B497B /* CreatorChannelFanTalkWriteViewModel.swift in Sources */,
3BEBC2633F5CCEC2118B497B /* CreatorChannelFanTalkWriteView.swift in Sources */,
3BEBC2643F5CCEC2118B497B /* SodaV2ActionModal.swift in Sources */,
409E35082B0759DE00CCB985 /* MoyaProviderExtension.swift in Sources */, 409E35082B0759DE00CCB985 /* MoyaProviderExtension.swift in Sources */,
40DDBD772B2B4FD700A2E8DA /* CreatorCommunityMoreItemView.swift in Sources */, 40DDBD772B2B4FD700A2E8DA /* CreatorCommunityMoreItemView.swift in Sources */,
400EA7742A89C42C004992CF /* StartLiveRequest.swift in Sources */, 400EA7742A89C42C004992CF /* StartLiveRequest.swift in Sources */,
@@ -7338,6 +7375,9 @@
CC0620082FF0600000A5BBA3 /* CreatorChannelCommunityGridItem.swift in Sources */, CC0620082FF0600000A5BBA3 /* CreatorChannelCommunityGridItem.swift in Sources */,
CC0620092FF0600000A5BBA3 /* CreatorChannelCommunityUploadButton.swift in Sources */, CC0620092FF0600000A5BBA3 /* CreatorChannelCommunityUploadButton.swift in Sources */,
CC06200A2FF0600000A5BBA3 /* CreatorChannelCommunityEmptyView.swift in Sources */, CC06200A2FF0600000A5BBA3 /* CreatorChannelCommunityEmptyView.swift in Sources */,
CE93595AF7A2BF7301F517DB /* CreatorChannelFanTalkWriteViewModel.swift in Sources */,
CE93595BF7A2BF7301F517DB /* CreatorChannelFanTalkWriteView.swift in Sources */,
CE93595CF7A2BF7301F517DB /* SodaV2ActionModal.swift in Sources */,
401B3F092FEEF10000A5BBA3 /* MainHomeRankingViewModel.swift in Sources */, 401B3F092FEEF10000A5BBA3 /* MainHomeRankingViewModel.swift in Sources */,
401A3FAC2FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */, 401A3FAC2FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */,
401A3FAD2FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */, 401A3FAD2FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */,
@@ -7411,6 +7451,9 @@
2BA6CE345486A0F75F7E75DA /* CreatorChannelFanTalkActionPopup.swift in Sources */, 2BA6CE345486A0F75F7E75DA /* CreatorChannelFanTalkActionPopup.swift in Sources */,
1FA2B186FCA7B8B83648CF77 /* CreatorChannelFanTalkEmptyView.swift in Sources */, 1FA2B186FCA7B8B83648CF77 /* CreatorChannelFanTalkEmptyView.swift in Sources */,
69DBCDB25941100E1CFF8C50 /* CreatorChannelFloatingIconButton.swift in Sources */, 69DBCDB25941100E1CFF8C50 /* CreatorChannelFloatingIconButton.swift in Sources */,
B0DD3E24BFA368AD7007AF23 /* CreatorChannelFanTalkWriteViewModel.swift in Sources */,
B0DD3E25BFA368AD7007AF23 /* CreatorChannelFanTalkWriteView.swift in Sources */,
B0DD3E26BFA368AD7007AF23 /* SodaV2ActionModal.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -8323,6 +8366,9 @@
4FDDF9E4DF4F003D30DDA8CE /* CreatorChannelFanTalkActionPopup.swift in Sources */, 4FDDF9E4DF4F003D30DDA8CE /* CreatorChannelFanTalkActionPopup.swift in Sources */,
2D761BB45AB6A332BE4690A1 /* CreatorChannelFanTalkEmptyView.swift in Sources */, 2D761BB45AB6A332BE4690A1 /* CreatorChannelFanTalkEmptyView.swift in Sources */,
5B9BFA224A49A5AF0757FDF7 /* CreatorChannelFloatingIconButton.swift in Sources */, 5B9BFA224A49A5AF0757FDF7 /* CreatorChannelFloatingIconButton.swift in Sources */,
5AE12C83F7B160F075A5A6A8 /* CreatorChannelFanTalkWriteViewModel.swift in Sources */,
5AE12C84F7B160F075A5A6A8 /* CreatorChannelFanTalkWriteView.swift in Sources */,
5AE12C85F7B160F075A5A6A8 /* SodaV2ActionModal.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@@ -128,6 +128,14 @@ enum AppStep {
case creatorCommunityModify(postId: Int, onSuccess: () -> Void) case creatorCommunityModify(postId: Int, onSuccess: () -> Void)
case creatorChannelFanTalkWrite(
creatorId: Int,
mode: CreatorChannelFanTalkWriteViewModel.Mode,
fanTalkId: Int?,
initialContent: String,
onSuccess: () -> Void
)
case canCoupon(refresh: () -> Void) case canCoupon(refresh: () -> Void)
case contentAllByTheme(themeId: Int) case contentAllByTheme(themeId: Int)

View File

@@ -254,6 +254,9 @@ struct AppStepLayerView: View {
case .creatorCommunityModify(let postId, let onSuccess): case .creatorCommunityModify(let postId, let onSuccess):
CreatorCommunityModifyView(postId: postId, onSuccess: onSuccess) CreatorCommunityModifyView(postId: postId, onSuccess: onSuccess)
case .creatorChannelFanTalkWrite(let creatorId, let mode, _, _, let onSuccess):
CreatorChannelFanTalkWriteView(creatorId: creatorId, mode: mode, onSuccess: onSuccess)
case .canCoupon(let refresh): case .canCoupon(let refresh):
CanCouponView(refresh: refresh) CanCouponView(refresh: refresh)

View File

@@ -3407,6 +3407,14 @@ If you block this user, the following features will be restricted.
} }
} }
enum CreatorChannelFanTalk {
static var send: String { pick(ko: "보내기", en: "Send", ja: "送信") }
static var exitModalTitle: String { pick(ko: "입력 종료", en: "Discard input", ja: "入力終了") }
static var exitModalMessage: String { pick(ko: "입력을 종료할까요?\n지금 나가면 입력한 내용이 저장되지 않아요.", en: "Discard your input?\nIf you leave now, your input will not be saved.", ja: "入力を終了しますか?\n今退出すると入力した内容は保存されません。") }
static var exitModalExit: String { pick(ko: "종료", en: "Discard", ja: "終了") }
static var exitModalContinue: String { pick(ko: "계속 작성", en: "Keep writing", ja: "続けて書く") }
}
enum HomeRecommendation { enum HomeRecommendation {
static var activityLive: String { static var activityLive: String {
pick(ko: "라이브", en: "Live", ja: "ライブ") pick(ko: "라이브", en: "Live", ja: "ライブ")

View File

@@ -0,0 +1,90 @@
import SwiftUI
struct SodaV2ActionModalButton {
let label: String?
let action: (() -> Void)?
}
struct SodaV2ActionModal: View {
let title: String
let message: String
let button1: SodaV2ActionModalButton?
let button2: SodaV2ActionModalButton?
let onDimmedTap: () -> Void
private var visibleButtons: [(label: String, action: () -> Void, isPrimary: Bool)] {
var buttons = [(label: String, action: () -> Void, isPrimary: Bool)]()
if let button2,
let label = button2.label,
let action = button2.action {
buttons.append((label, action, false))
}
if let button1,
let label = button1.label,
let action = button1.action {
buttons.append((label, action, true))
}
return buttons
}
var body: some View {
ZStack {
Color.black.opacity(0.6)
.ignoresSafeArea()
.onTapGesture(perform: onDimmedTap)
VStack(spacing: 0) {
Text(title)
.appFont(size: 20, weight: .bold)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.frame(width: 340, height: 64)
Text(message)
.appFont(size: 16, weight: .regular)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.padding(20)
.frame(width: 340)
buttonArea
.padding(.horizontal, 20)
.padding(.vertical, 14)
.frame(width: 340)
}
.background(Color.gray900)
.cornerRadius(14)
}
}
@ViewBuilder
private var buttonArea: some View {
let buttons = visibleButtons
if buttons.count == 1, let button = buttons.first {
actionButton(title: button.label, isPrimary: true, action: button.action)
} else if buttons.count == 2 {
HStack(spacing: 8) {
ForEach(Array(buttons.enumerated()), id: \.offset) { _, button in
actionButton(title: button.label, isPrimary: button.isPrimary, action: button.action)
}
}
}
}
private func actionButton(title: String, isPrimary: Bool, action: @escaping () -> Void) -> some View {
Button(action: action) {
Text(title)
.appFont(size: 18, weight: .medium)
.foregroundColor(isPrimary ? Color.soda400 : Color.white)
.frame(maxWidth: .infinity)
.padding(14)
.background(Color.clear)
.cornerRadius(100)
}
.buttonStyle(.plain)
}
}

View File

@@ -291,7 +291,8 @@ struct CreatorChannelView: View {
creatorId: creatorId, creatorId: creatorId,
isOwnCreatorChannel: isOwnCreatorChannel, isOwnCreatorChannel: isOwnCreatorChannel,
viewModel: fanTalkViewModel, viewModel: fanTalkViewModel,
onTapWrite: showFanTalkWriteView onTapWrite: showFanTalkWriteView,
onTapEdit: showFanTalkModifyView
) )
} else if viewModel.selectedTab == .community { } else if viewModel.selectedTab == .community {
CreatorChannelCommunityTabView( CreatorChannelCommunityTabView(
@@ -599,7 +600,35 @@ struct CreatorChannelView: View {
communityViewModel.selectedPostId = 0 communityViewModel.selectedPostId = 0
} }
private func showFanTalkWriteView() {} private func showFanTalkWriteView() {
AppState.shared.setAppStep(
step: .creatorChannelFanTalkWrite(
creatorId: creatorId,
mode: .write,
fanTalkId: nil,
initialContent: "",
onSuccess: {
viewModel.fetchHome(creatorId: creatorId)
fanTalkViewModel.fetchFirstPage(creatorId: creatorId)
}
)
)
}
private func showFanTalkModifyView(_ fanTalk: CreatorChannelFanTalkItemResponse) {
AppState.shared.setAppStep(
step: .creatorChannelFanTalkWrite(
creatorId: creatorId,
mode: .modify(fanTalkId: fanTalk.fanTalkId, initialContent: fanTalk.content),
fanTalkId: fanTalk.fanTalkId,
initialContent: fanTalk.content,
onSuccess: {
viewModel.fetchHome(creatorId: creatorId)
fanTalkViewModel.fetchFirstPage(creatorId: creatorId)
}
)
)
}
} }
struct CreatorChannelView_Previews: PreviewProvider { struct CreatorChannelView_Previews: PreviewProvider {

View File

@@ -5,6 +5,7 @@ struct CreatorChannelFanTalkTabView: View {
let isOwnCreatorChannel: Bool let isOwnCreatorChannel: Bool
@ObservedObject var viewModel: CreatorChannelFanTalkViewModel @ObservedObject var viewModel: CreatorChannelFanTalkViewModel
let onTapWrite: () -> Void let onTapWrite: () -> Void
let onTapEdit: (CreatorChannelFanTalkItemResponse) -> Void
@State private var actionPopupFanTalk: CreatorChannelFanTalkItemResponse? @State private var actionPopupFanTalk: CreatorChannelFanTalkItemResponse?
@State private var fanTalkItemFrames = [Int: CGRect]() @State private var fanTalkItemFrames = [Int: CGRect]()
@@ -98,6 +99,7 @@ struct CreatorChannelFanTalkTabView: View {
isOwnCreatorChannel: isOwnCreatorChannel, isOwnCreatorChannel: isOwnCreatorChannel,
onTapEdit: { onTapEdit: {
actionPopupFanTalk = nil actionPopupFanTalk = nil
onTapEdit(fanTalk)
}, },
onTapDelete: { onTapDelete: {
actionPopupFanTalk = nil actionPopupFanTalk = nil

View File

@@ -0,0 +1,146 @@
import SwiftUI
struct CreatorChannelFanTalkWriteView: View {
@StateObject private var viewModel: CreatorChannelFanTalkWriteViewModel
@FocusState private var isFocused: Bool
let onSuccess: () -> Void
init(
creatorId: Int,
mode: CreatorChannelFanTalkWriteViewModel.Mode,
onSuccess: @escaping () -> Void
) {
_viewModel = StateObject(wrappedValue: CreatorChannelFanTalkWriteViewModel(creatorId: creatorId, mode: mode))
self.onSuccess = onSuccess
}
var body: some View {
BaseView(isLoading: $viewModel.isLoading) {
ZStack(alignment: .bottom) {
Color.black
.ignoresSafeArea()
.onTapGesture { hideKeyboard() }
VStack(spacing: 0) {
navigationBar
TextEditor(text: $viewModel.content)
.appFont(size: 16, weight: .regular)
.foregroundColor(Color.white)
.accentColor(Color.soda400)
.scrollContentBackground(.hidden)
.background(Color.black)
.focused($isFocused)
.padding(.horizontal, SodaSpacing.s14)
.padding(.top, SodaSpacing.s20)
.padding(.bottom, 52)
}
characterCountBar
}
.navigationBarBackButtonHidden(true)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
isFocused = true
}
}
.overlay(exitModal)
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 1)
}
}
private var navigationBar: some View {
HStack(spacing: 0) {
Button(action: cancel) {
Text(I18n.Common.cancel)
.appFont(size: 16, weight: .regular)
.foregroundColor(Color.white)
.frame(width: 64, height: 56, alignment: .leading)
}
.buttonStyle(.plain)
Spacer()
Button(action: submit) {
Text(I18n.CreatorChannelFanTalk.send)
.appFont(size: 16, weight: .medium)
.foregroundColor(Color.white)
.padding(.horizontal, SodaSpacing.s12)
.frame(height: 44)
.background(viewModel.canSend ? Color.soda400 : Color.gray800)
.clipShape(Capsule())
}
.buttonStyle(.plain)
.disabled(viewModel.canSend == false)
}
.padding(.horizontal, SodaSpacing.s14)
.frame(height: 56)
.background(Color.black)
}
private var characterCountBar: some View {
HStack(spacing: 0) {
Spacer()
Text("\(viewModel.content.count)")
.appFont(size: 16, weight: .medium)
.foregroundColor(Color.white)
Text("/\(viewModel.maxContentCount)")
.appFont(size: 16, weight: .medium)
.foregroundColor(Color.gray500)
}
.padding(.horizontal, SodaSpacing.s14)
.frame(height: 52)
.background(Color.black)
.overlay(alignment: .top) {
Rectangle()
.fill(Color.gray700)
.frame(height: 1)
}
}
@ViewBuilder
private var exitModal: some View {
if viewModel.shouldShowExitModal {
SodaV2ActionModal(
title: I18n.CreatorChannelFanTalk.exitModalTitle,
message: I18n.CreatorChannelFanTalk.exitModalMessage,
button1: SodaV2ActionModalButton(
label: I18n.CreatorChannelFanTalk.exitModalContinue,
action: {
viewModel.shouldShowExitModal = false
}
),
button2: SodaV2ActionModalButton(
label: I18n.CreatorChannelFanTalk.exitModalExit,
action: {
viewModel.shouldShowExitModal = false
AppState.shared.back()
}
),
onDimmedTap: {
viewModel.shouldShowExitModal = false
}
)
}
}
private func cancel() {
hideKeyboard()
if viewModel.handleCancel() {
AppState.shared.back()
}
}
private func submit() {
hideKeyboard()
viewModel.submit {
AppState.shared.back()
DispatchQueue.main.async {
onSuccess()
}
}
}
}

View File

@@ -0,0 +1,130 @@
import Foundation
import Combine
final class CreatorChannelFanTalkWriteViewModel: ObservableObject {
enum Mode {
case write
case modify(fanTalkId: Int, initialContent: String)
var initialContent: String {
if case .modify(_, let initialContent) = self {
return initialContent
}
return ""
}
}
private let repository = ExplorerRepository()
private var subscription = Set<AnyCancellable>()
let creatorId: Int
let mode: Mode
@Published var content: String
@Published var isLoading = false
@Published var errorMessage = ""
@Published var isShowPopup = false
@Published var shouldShowExitModal = false
let maxContentCount = 500
var canSend: Bool {
content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
}
init(creatorId: Int, mode: Mode) {
self.creatorId = creatorId
self.mode = mode
content = mode.initialContent
}
func handleCancel() -> Bool {
guard canSend else { return true }
shouldShowExitModal = true
return false
}
func submit(onSuccess: @escaping () -> Void) {
guard canSend, isLoading == false else { return }
switch mode {
case .write:
writeCheers(onSuccess: onSuccess)
case .modify(let fanTalkId, _):
modifyCheers(fanTalkId: fanTalkId, onSuccess: onSuccess)
}
}
private func writeCheers(onSuccess: @escaping () -> Void) {
isLoading = true
repository.writeCheers(parentCheersId: nil, creatorId: creatorId, content: content)
.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
self?.handleMutationResponse(response.data, onSuccess: onSuccess)
}
.store(in: &subscription)
}
private func modifyCheers(fanTalkId: Int, onSuccess: @escaping () -> Void) {
guard fanTalkId > 0 else {
errorMessage = I18n.Common.commonError
isShowPopup = true
return
}
isLoading = true
repository.modifyCheers(cheersId: fanTalkId, content: content, isActive: nil)
.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
self?.handleMutationResponse(response.data, onSuccess: onSuccess)
}
.store(in: &subscription)
}
private func handleMutationResponse(_ data: Data, onSuccess: @escaping () -> Void) {
isLoading = false
do {
let decoded = try JSONDecoder().decode(ApiResponseWithoutData.self, from: data)
if decoded.success {
onSuccess()
} else {
errorMessage = decoded.message ?? I18n.Common.commonError
isShowPopup = true
}
} catch {
ERROR_LOG(error.localizedDescription)
errorMessage = I18n.Common.commonError
isShowPopup = true
}
}
private func applyFailureState() {
isLoading = false
errorMessage = I18n.Common.commonError
isShowPopup = true
}
}

View File

@@ -12,6 +12,13 @@
- 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 아이템 뷰(답글 있음): `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 더보기 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` - 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`
- Figma 글 입력(입력 값 없음, 키보드 있음): `290:9235`, `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-9235&m=dev`
- Figma 글 입력(입력 값 있음, 키보드 있음): `290:9282`, `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-9282&m=dev`
- Figma 글 입력(입력 값 있음, 키보드 없음): `290:9290`, `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-9290&m=dev`
- Figma 입력 종료 모달: `308:19046`, `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=308-19046&m=dev`
- Figma 공용 모달(버튼 1개): `567:17662`, `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=567-17662&m=dev`
- Figma 공용 모달(버튼 2개): `567:17666`, `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=567-17666&m=dev`
- Figma 공용 모달 dimmed 포함: `567:17712`, `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=567-17712&m=dev`
## 구현 원칙 ## 구현 원칙
@@ -23,9 +30,15 @@
- 삭제는 기존 `ExplorerRepository.modifyCheers(cheersId:content:isActive:)``isActive=false`를 전달하는 흐름을 재사용한다. - 삭제는 기존 `ExplorerRepository.modifyCheers(cheersId:content:isActive:)``isActive=false`를 전달하는 흐름을 재사용한다.
- 더보기 버튼은 `ic_new_more`를 사용한다. - 더보기 버튼은 `ic_new_more`를 사용한다.
- 내가 쓴 글이면 `수정하기`, `삭제하기`를 표시하고, 내가 쓴 글이 아니지만 내 채널이면 `삭제하기`만 표시한다. - 내가 쓴 글이면 `수정하기`, `삭제하기`를 표시하고, 내가 쓴 글이 아니지만 내 채널이면 `삭제하기`만 표시한다.
- 글쓰기와 수정하기 action은 신규 글쓰기 페이지 구현 전까지 no-op placeholder로 둔다. - 글쓰기와 수정하기 action은 이번에 작성하는 팬Talk 쓰기 페이지로 연결한다.
- 작성 API는 기존 팬Talk API인 `ExplorerRepository.writeCheers(parentCheersId:creatorId:content:)`를 사용한다.
- 수정 API는 기존 팬Talk API인 `ExplorerRepository.modifyCheers(cheersId:content:isActive:)`를 사용하고 `isActive``nil`로 전달한다.
- 내 채널인 경우 우측하단 글쓰기 floating button과 empty view 내부 `응원 남기기` 버튼을 모두 표시하지 않는다. - 내 채널인 경우 우측하단 글쓰기 floating button과 empty view 내부 `응원 남기기` 버튼을 모두 표시하지 않는다.
- 우측하단 plus button은 `CreatorChannelFloatingActionMenu`의 private `floatingButton`과 동일한 UI이므로, 팬Talk 전용으로 별도 구현하지 않고 `CreatorChannel/Components` 하위 공용 icon floating button으로 추출해 재사용한다. - 우측하단 plus button은 `CreatorChannelFloatingActionMenu`의 private `floatingButton`과 동일한 UI이므로, 팬Talk 전용으로 별도 구현하지 않고 `CreatorChannel/Components` 하위 공용 icon floating button으로 추출해 재사용한다.
- 팬Talk 쓰기 화면은 크리에이터 채널 팬Talk 탭의 우측하단 plus button, empty state의 `응원 남기기` 버튼, 내가 쓴 글의 `수정하기`에서 진입한다.
- 팬Talk 쓰기 화면 진입 시 키보드가 올라온 상태를 기본으로 하며, 입력 값 유무에 따라 보내기 버튼 disabled/enabled 상태를 분리한다.
- 입력 값이 있는 상태에서 취소 버튼을 터치하면 입력 종료 안내 모달을 표시한다. 입력 값이 없으면 별도 모달 없이 이전 화면으로 닫는다.
- 공용 모달은 요청 명칭상 `V2/Components` 대상이지만, 현재 저장소 경로 관례에 맞춰 `SodaLive/Sources/V2/Component/Modal/**` 아래에 작성한다.
## 주요 대상 파일 ## 주요 대상 파일
@@ -40,12 +53,18 @@
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkActionPopup.swift` - `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkActionPopup.swift`
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkEmptyView.swift` - `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkEmptyView.swift`
- `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingIconButton.swift` - `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingIconButton.swift`
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/Write/CreatorChannelFanTalkWriteView.swift`
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/Write/CreatorChannelFanTalkWriteViewModel.swift`
- `SodaLive/Sources/V2/Component/Modal/SodaV2ActionModal.swift`
### 수정 ### 수정
- `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift` - `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
- `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingActionMenu.swift` - `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingActionMenu.swift`
- `SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeResponse.swift` - `SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeResponse.swift`
- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelFanTalkSection.swift` - `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelFanTalkSection.swift`
- `SodaLive/Sources/App/AppStep.swift`
- `SodaLive/Sources/ContentView.swift`
- `SodaLive/Sources/Explorer/ExplorerRepository.swift`
- `SodaLive/Sources/I18n/I18n.swift` - `SodaLive/Sources/I18n/I18n.swift`
- 필요 시 `SodaLive.xcodeproj/project.pbxproj` - 필요 시 `SodaLive.xcodeproj/project.pbxproj`
@@ -191,7 +210,7 @@
- `@StateObject`로 팬Talk 탭 ViewModel을 보유한다. - `@StateObject`로 팬Talk 탭 ViewModel을 보유한다.
- `selectedTabContent`에서 `.fanTalk`일 때 `CreatorChannelFanTalkTabView`를 표시한다. - `selectedTabContent`에서 `.fanTalk`일 때 `CreatorChannelFanTalkTabView`를 표시한다.
- 우측하단 글쓰기 floating button은 `selectedTab == .fanTalk`, 내 채널이 아니고 `fanTalkCount > 0`일 때만 `CreatorChannelFloatingIconButton`으로 표시한다. - 우측하단 글쓰기 floating button은 `selectedTab == .fanTalk`, 내 채널이 아니고 `fanTalkCount > 0`일 때만 `CreatorChannelFloatingIconButton`으로 표시한다.
- 글쓰기 action은 신규 글쓰기 페이지 구현 전까지 no-op placeholder로 둔다. - 글쓰기 action은 Phase 8에서 팬Talk 쓰기 페이지 작성 모드로 연결한다.
- 검증 기준: - 검증 기준:
- 실행 명령: `rg "CreatorChannelFanTalkViewModel|CreatorChannelFanTalkTabView|viewModel\\.selectedTab == \\.fanTalk|CreatorChannelFloatingIconButton|showFanTalkWrite|fanTalkCount|ic_plus_no_bg" SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift` - 실행 명령: `rg "CreatorChannelFanTalkViewModel|CreatorChannelFanTalkTabView|viewModel\\.selectedTab == \\.fanTalk|CreatorChannelFloatingIconButton|showFanTalkWrite|fanTalkCount|ic_plus_no_bg" SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
@@ -215,6 +234,134 @@
- 기대 결과: `BUILD SUCCEEDED`. - 기대 결과: `BUILD SUCCEEDED`.
- workspace 인식 실패 등 환경 문제가 있으면 `xcodebuild -project "SodaLive.xcodeproj" -scheme "SodaLive-dev" -configuration Debug build`를 대체 검증으로 실행하고 실패 원인을 검증 기록에 남긴다. - workspace 인식 실패 등 환경 문제가 있으면 `xcodebuild -project "SodaLive.xcodeproj" -scheme "SodaLive-dev" -configuration Debug build`를 대체 검증으로 실행하고 실패 원인을 검증 기록에 남긴다.
### Phase 6: 팬Talk 글 입력 문서와 공용 모달
- [x] **Task 6.1: 팬Talk 쓰기 Figma URL과 후속 구현 범위 기록**
- 대상 파일:
- 수정: `docs/20260704_크리에이터_채널_팬Talk_탭/prd.md`
- 수정: `docs/20260704_크리에이터_채널_팬Talk_탭/plan-task.md`
- 작업 내용:
- 팬Talk 쓰기 화면 Figma URL `290:9235`, `290:9282`, `290:9290`을 기록한다.
- 입력 종료 모달 Figma URL `308:19046`을 기록한다.
- 공용 모달 Figma URL `567:17662`, `567:17666`, `567:17712`을 기록한다.
- PRD에 팬Talk 작성/수정 진입, 입력 상태별 보내기 버튼, 입력 종료 안내 모달, 공용 모달 요구사항을 기록한다.
- 이번 작업은 문서만 작성하고 Swift 구현은 후속 task로 남긴다.
- 검증 기준:
- 실행 명령: `rg "290:9235|290:9282|290:9290|308:19046|567:17662|567:17666|567:17712|SodaV2ActionModal|ExplorerRepository.writeCheers|ExplorerRepository.modifyCheers|수정 모드" docs/20260704_크리에이터_채널_팬Talk_탭/prd.md docs/20260704_크리에이터_채널_팬Talk_탭/plan-task.md`
- 기대 결과: 팬Talk 쓰기와 공용 모달 Figma node, 공용 모달, 기존 작성/수정 API 사용 방침이 PRD와 plan-task에 기록된다.
- [x] **Task 6.2: V2 공용 액션 모달 구현**
- 대상 파일:
- 생성: `SodaLive/Sources/V2/Component/Modal/SodaV2ActionModal.swift`
- 작업 내용:
- Figma `567:17662`, `567:17666`, `567:17712` 기준의 공용 모달을 만든다.
- `title`, `message`, `button1`, `button2`, `onDimmedTap`을 외부에서 주입받는다.
- `button1`은 오른쪽, `button2`는 왼쪽에 배치한다.
- 각 버튼은 label과 action이 모두 있을 때만 표시한다.
- 버튼 1개만 표시될 때는 Figma `567:17662` 기준으로 단일 버튼 레이아웃을 사용한다.
- 버튼 2개가 표시될 때는 Figma `567:17666` 기준으로 왼쪽 `button2`, 오른쪽 `button1` 순서로 배치한다.
- dimmed는 `Color.black.opacity(0.6)`로 화면 전체를 덮고, dimmed tap 시 별도 액션 없이 모달만 닫히도록 `onDimmedTap`을 호출한다.
- 기존 `SodaDialog`는 구 UI와 opacity 50% 규칙을 사용하므로 이번 V2 Figma 모달 구현에는 직접 수정하지 않는다.
- 검증 기준:
- 실행 명령: `rg "struct SodaV2ActionModal|button1|button2|onDimmedTap|Color\\.black\\.opacity\\(0\\.6\\)" SodaLive/Sources/V2/Component/Modal/SodaV2ActionModal.swift`
- 기대 결과: 공용 모달의 주입값, 버튼 표시 조건, dimmed 규칙이 코드에서 확인된다.
### Phase 7: 팬Talk 쓰기 화면
- [x] **Task 7.1: 팬Talk 쓰기 ViewModel 구현**
- 대상 파일:
- 생성: `SodaLive/Sources/V2/CreatorChannel/FanTalk/Write/CreatorChannelFanTalkWriteViewModel.swift`
- 확인/필요 시 수정: `SodaLive/Sources/Explorer/ExplorerRepository.swift`
- 작업 내용:
- 작성/수정 mode를 구분하는 enum을 둔다.
- `@Published var content`를 둔다.
- 수정 모드는 route에서 받은 `fanTalkId`와 기존 content를 필수 입력값으로 사용한다.
- 수정 모드의 `content` 초기값은 route에서 받은 기존 content와 같아야 한다.
- 수정 모드의 `fanTalkId`는 0보다 커야 한다.
- `canSend``content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false`일 때만 `true`가 되게 한다.
- `shouldShowExitModal`은 입력 값이 있을 때 취소 버튼을 누르면 `true`로 전환한다.
- 작성 mode의 보내기 action은 기존 `ExplorerRepository.writeCheers(parentCheersId:nil, creatorId:content:)`를 호출한다.
- 수정 mode의 보내기 action은 기존 `ExplorerRepository.modifyCheers(cheersId:fanTalkId, content:content, isActive:nil)`를 호출한다.
- 작성/수정 성공 시 `onSuccess`를 호출하고 화면을 닫을 수 있도록 성공 callback을 분리한다.
- 검증 기준:
- 실행 명령: `rg "CreatorChannelFanTalkWriteViewModel|enum .*Mode|fanTalkId|initialContent|@Published var content|canSend|trimmingCharacters|shouldShowExitModal|writeCheers|modifyCheers|isActive: nil|onSuccess" SodaLive/Sources/V2/CreatorChannel/FanTalk/Write/CreatorChannelFanTalkWriteViewModel.swift SodaLive/Sources/Explorer/ExplorerRepository.swift`
- 기대 결과: 입력 값 기반 send 활성화 조건, 수정 모드의 `fanTalkId`/기존 content 필수 전달, 기존 작성/수정 API 사용 지점이 확인된다.
- [x] **Task 7.2: 팬Talk 쓰기 View 구현**
- 대상 파일:
- 생성: `SodaLive/Sources/V2/CreatorChannel/FanTalk/Write/CreatorChannelFanTalkWriteView.swift`
- 수정: `SodaLive/Sources/I18n/I18n.swift`
- 작업 내용:
- Figma `290:9235` 기준으로 입력 값이 없을 때 보내기 버튼을 비활성화한다.
- Figma `290:9282` 기준으로 입력 값이 있을 때 보내기 버튼을 활성화한다.
- Figma `290:9290` 기준으로 입력 값이 있고 키보드가 내려간 상태의 레이아웃을 지원한다.
- 화면 진입 시 `FocusState`로 입력 영역에 focus를 주어 키보드가 올라오게 한다.
- 수정 모드 진입 시 route에서 받은 기존 content가 입력 영역에 표시되도록 ViewModel 초기값을 사용한다.
- 배경 tap 또는 기존 keyboard helper로 키보드를 내릴 수 있게 하되, 입력 값은 유지한다.
- 취소 버튼 tap 시 입력 값이 있으면 `SodaV2ActionModal`로 입력 종료 안내 모달을 표시하고, 입력 값이 없으면 이전 화면으로 닫는다.
- 입력 종료 모달은 Figma `308:19046` 기준으로 표시한다.
- 입력 종료 모달의 dimmed tap은 모달만 닫고 입력 화면은 유지한다.
- 검증 기준:
- 실행 명령: `rg "CreatorChannelFanTalkWriteView|FocusState|focused\\(|canSend|SodaV2ActionModal|shouldShowExitModal|initialContent|mode" SodaLive/Sources/V2/CreatorChannel/FanTalk/Write/CreatorChannelFanTalkWriteView.swift`
- 기대 결과: 입력 상태별 UI, keyboard focus, 수정 모드 초기값, 취소 시 모달 표시가 코드에서 확인된다.
### Phase 8: 팬Talk 쓰기 진입 연결
- [x] **Task 8.1: AppStep route 추가**
- 대상 파일:
- 수정: `SodaLive/Sources/App/AppStep.swift`
- 수정: `SodaLive/Sources/ContentView.swift`
- 작업 내용:
- `AppStep`에 팬Talk 쓰기 route를 추가한다.
- route parameter는 최소 `creatorId`, 작성/수정 mode, 수정 대상 `fanTalkId`, 초기 content, `onSuccess`를 포함한다.
- 작성 모드는 `fanTalkId`와 초기 content를 전달하지 않거나 비어 있는 값으로 전달한다.
- 수정 모드는 수정 대상 `fanTalkId`와 기존 글 content를 반드시 함께 전달한다.
- `ContentView``AppStepLayerView`에서 route를 `CreatorChannelFanTalkWriteView`로 연결한다.
- 작성/수정 성공 시 `onSuccess`를 호출해 팬Talk 첫 페이지를 재조회할 수 있게 한다.
- 검증 기준:
- 실행 명령: `rg "creatorChannelFanTalkWrite|CreatorChannelFanTalkWriteView|onSuccess|fanTalkId|initialContent|mode" SodaLive/Sources/App/AppStep.swift SodaLive/Sources/ContentView.swift`
- 기대 결과: route와 화면 연결이 확인된다.
- [x] **Task 8.2: CreatorChannel 팬Talk 탭 진입 action 연결**
- 대상 파일:
- 수정: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
- 확인: `SodaLive/Sources/V2/CreatorChannel/FanTalk/CreatorChannelFanTalkTabView.swift`
- 확인: `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkEmptyView.swift`
- 작업 내용:
- 크리에이터 채널 팬Talk 탭 우측하단 plus button tap 시 팬Talk 쓰기 route를 작성 모드로 이동한다.
- empty state의 `응원 남기기` 버튼 tap 시 같은 팬Talk 쓰기 route를 작성 모드로 이동한다.
- 내가 쓴 글의 `수정하기` tap 시 같은 팬Talk 쓰기 route를 수정 모드로 이동하고 수정 대상 `fanTalkId`, 기존 글 content를 반드시 함께 전달한다.
- 작성/수정 전송 성공 후 `fanTalkViewModel.fetchFirstPage(creatorId:)`를 호출하도록 `onSuccess`를 전달한다.
- 내 채널에서는 기존 규칙대로 plus button과 empty state `응원 남기기` 버튼을 표시하지 않는다.
- 검증 기준:
- 실행 명령: `rg "showFanTalkWriteView|showFanTalkModifyView|creatorChannelFanTalkWrite|fetchFirstPage\\(creatorId: creatorId\\)|onTapWrite|onTapEdit|응원 남기기|fanTalkId|content|initialContent" SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift SodaLive/Sources/V2/CreatorChannel/FanTalk/CreatorChannelFanTalkTabView.swift SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkEmptyView.swift`
- 기대 결과: plus button, empty state button, 수정하기가 같은 팬Talk 쓰기 route로 연결되고 수정 모드는 `fanTalkId`와 기존 글 content를 함께 전달한다.
### Phase 9: 팬Talk 쓰기 검증
- [x] **Task 9.1: 정적 검증**
- 대상 파일:
- 확인: `docs/20260704_크리에이터_채널_팬Talk_탭/plan-task.md`
- 확인: `SodaLive/Sources/V2/Component/Modal`
- 확인: `SodaLive/Sources/V2/CreatorChannel/FanTalk/Write`
- 확인: `SodaLive/Sources/App/AppStep.swift`
- 확인: `SodaLive/Sources/ContentView.swift`
- 확인: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
- 검증 기준:
- 실행 명령: `git diff --check -- docs/20260704_크리에이터_채널_팬Talk_탭 SodaLive/Sources/V2/Component/Modal SodaLive/Sources/V2/CreatorChannel/FanTalk/Write SodaLive/Sources/App/AppStep.swift SodaLive/Sources/ContentView.swift SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift SodaLive/Sources/I18n/I18n.swift SodaLive.xcodeproj/project.pbxproj`
- 기대 결과: 공백 오류 없이 성공한다.
- 실행 명령: `plutil -lint SodaLive.xcodeproj/project.pbxproj`
- 기대 결과: `OK` 출력.
- [x] **Task 9.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: Figma `get_design_context``get_screenshot`으로 `290:9139`, `290:9145`, `290:9147`, `666:36114`, `290:9001`을 확인했다.
@@ -226,3 +373,19 @@
- 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: `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" -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.`으로 실패했다. 현재 범위에서는 새 테스트 타깃을 추가하지 않고 빌드/정적 검증으로 대체했다. - 2026-07-04: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test``Scheme SodaLive-dev is not currently configured for the test action.`으로 실패했다. 현재 범위에서는 새 테스트 타깃을 추가하지 않고 빌드/정적 검증으로 대체했다.
- 2026-07-06: 사용자 요청에 따라 Swift 구현 없이 `prd.md``plan-task.md`에 팬Talk 글 입력 화면 Figma URL(`290:9235`, `290:9282`, `290:9290`), 입력 종료 모달(`308:19046`), 공용 모달(`567:17662`, `567:17666`, `567:17712`)과 후속 구현 task를 추가했다.
- 2026-07-06: `rg "290:9235|290:9282|290:9290|308:19046|567:17662|567:17666|567:17712|SodaV2ActionModal|ExplorerRepository.writeCheers" docs/20260704_크리에이터_채널_팬Talk_탭/prd.md docs/20260704_크리에이터_채널_팬Talk_탭/plan-task.md`로 Figma node, 공용 모달, 기존 작성 API 재사용 방침이 문서에 포함된 것을 확인했다.
- 2026-07-06: `git diff --check -- docs/20260704_크리에이터_채널_팬Talk_탭/prd.md docs/20260704_크리에이터_채널_팬Talk_탭/plan-task.md` 실행 결과 출력 없이 종료되어 공백 오류가 없음을 확인했다.
- 2026-07-06: 사용자 요청에 따라 팬Talk 작성/수정 모두 같은 쓰기 페이지에서 수행하고, 작성/수정 API는 기존 팬Talk API(`ExplorerRepository.writeCheers`, `ExplorerRepository.modifyCheers`)를 사용하도록 계획을 보완했다.
- 2026-07-06: `rg "작성/수정|쓰기 페이지|수정 모드|작성 모드|ExplorerRepository.writeCheers|ExplorerRepository.modifyCheers|modifyCheers\\(cheersId:fanTalkId, content:content, isActive:nil\\)|creatorChannelFanTalkWrite|showFanTalkModifyView" docs/20260704_크리에이터_채널_팬Talk_탭/prd.md docs/20260704_크리에이터_채널_팬Talk_탭/plan-task.md`로 작성/수정 공용 쓰기 페이지와 기존 팬Talk API 사용 방침을 확인했다.
- 2026-07-06: `rg -n "no-op|수정 페이지는|수정 mutation|작성용 새 API|재사용 가능 여부|불가능한 것이 확인|미결정 사항|수정하기 tap은 수정 페이지" docs/20260704_크리에이터_채널_팬Talk_탭/prd.md docs/20260704_크리에이터_채널_팬Talk_탭/plan-task.md` 실행 결과 매칭이 없어 이전 충돌 문구가 남지 않았음을 확인했다.
- 2026-07-06: 사용자 확인에 따라 수정 모드 진입 시 수정 대상 `fanTalkId`와 기존 글 content를 반드시 함께 전달하도록 계획을 보완했다.
- 2026-07-06: `rg '수정 모드.*fanTalkId|fanTalkId.*기존 글 content|fanTalkId.*초기 content|fanTalkId.*initialContent|기존 content.*content 초기값|수정 대상.*fanTalkId.*기존 글 content|fanTalkId.*0보다 커야' docs/20260704_크리에이터_채널_팬Talk_탭/prd.md docs/20260704_크리에이터_채널_팬Talk_탭/plan-task.md`로 수정 모드 route에 `fanTalkId`와 기존 글 content를 함께 전달한다는 조건을 확인했다.
- 2026-07-06: Phase 6.2~8.2 구현을 완료했다. `SodaV2ActionModal`, `CreatorChannelFanTalkWriteViewModel`, `CreatorChannelFanTalkWriteView`를 추가하고 `AppStep.creatorChannelFanTalkWrite`, `ContentView` route, `CreatorChannelView`의 plus/empty/edit 진입 action을 연결했다.
- 2026-07-06: `rg "struct SodaV2ActionModal|button1|button2|onDimmedTap|Color\.black\.opacity\(0\.6\)" SodaLive/Sources/V2/Component/Modal/SodaV2ActionModal.swift`로 V2 공용 액션 모달의 주입값, 버튼 표시 조건, dimmed 규칙을 확인했다. Figma node id는 코드 주석으로 남기지 않고 문서 기준으로만 유지했다.
- 2026-07-06: `rg "CreatorChannelFanTalkWriteViewModel|enum .*Mode|fanTalkId|initialContent|@Published var content|canSend|trimmingCharacters|shouldShowExitModal|writeCheers|modifyCheers|isActive: nil|onSuccess" SodaLive/Sources/V2/CreatorChannel/FanTalk/Write/CreatorChannelFanTalkWriteViewModel.swift SodaLive/Sources/Explorer/ExplorerRepository.swift`로 작성/수정 mode, 입력값 검증, 기존 작성/수정 API 재사용을 확인했다.
- 2026-07-06: `rg "CreatorChannelFanTalkWriteView|FocusState|focused\(|canSend|SodaV2ActionModal|shouldShowExitModal|initialContent|mode" SodaLive/Sources/V2/CreatorChannel/FanTalk/Write/CreatorChannelFanTalkWriteView.swift`로 쓰기 화면의 focus, send 활성화, 수정 모드 초기값, 취소 모달 연결을 확인했다.
- 2026-07-06: `rg "creatorChannelFanTalkWrite|CreatorChannelFanTalkWriteView|onSuccess|fanTalkId|initialContent|mode" SodaLive/Sources/App/AppStep.swift SodaLive/Sources/ContentView.swift``rg "showFanTalkWriteView|showFanTalkModifyView|creatorChannelFanTalkWrite|fetchFirstPage\(creatorId: creatorId\)|onTapWrite|onTapEdit|응원 남기기|fanTalkId|content|initialContent" SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift SodaLive/Sources/V2/CreatorChannel/FanTalk/CreatorChannelFanTalkTabView.swift SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkEmptyView.swift`로 작성/수정 route와 진입 action 연결을 확인했다.
- 2026-07-06: `plutil -lint SodaLive.xcodeproj/project.pbxproj` 결과 `OK`를 확인했다.
- 2026-07-06: `git diff --check -- docs/20260704_크리에이터_채널_팬Talk_탭 SodaLive/Sources/V2/Component/Modal SodaLive/Sources/V2/CreatorChannel/FanTalk/Write SodaLive/Sources/App/AppStep.swift SodaLive/Sources/ContentView.swift SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift SodaLive/Sources/V2/CreatorChannel/FanTalk/CreatorChannelFanTalkTabView.swift SodaLive/Sources/I18n/I18n.swift SodaLive.xcodeproj/project.pbxproj` 실행 결과 출력 없이 종료되어 공백 오류가 없음을 확인했다.
- 2026-07-06: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` 결과 `BUILD SUCCEEDED`를 확인했다. 빌드 중 `Alamofire` dependency scan warning이 출력되었으나 빌드는 성공했다.

View File

@@ -11,11 +11,21 @@ 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` - 아이템 뷰(답글 있음): `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` - 더보기 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` - 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`
- 글 입력(입력 값 없음, 키보드 있음): `290:9235`, `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-9235&m=dev`
- 글 입력(입력 값 있음, 키보드 있음): `290:9282`, `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-9282&m=dev`
- 글 입력(입력 값 있음, 키보드 없음): `290:9290`, `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-9290&m=dev`
- 입력 종료 모달: `308:19046`, `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=308-19046&m=dev`
- 공용 모달(버튼 1개): `567:17662`, `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=567-17662&m=dev`
- 공용 모달(버튼 2개): `567:17666`, `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=567-17666&m=dev`
- 공용 모달 dimmed 포함: `567:17712`, `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=567-17712&m=dev`
## 2. Problem ## 2. Problem
- 현재 `CreatorChannelTab.fanTalk`은 placeholder로 표시되어 팬Talk 목록을 확인할 수 없다. - 현재 `CreatorChannelTab.fanTalk`은 placeholder로 표시되어 팬Talk 목록을 확인할 수 없다.
- 기존 홈 탭에는 최신 팬Talk 요약만 있고, 팬Talk 탭 전용 목록 API, 페이징, 답글 표시, 신고/삭제 액션이 없다. - 기존 홈 탭에는 최신 팬Talk 요약만 있고, 팬Talk 탭 전용 목록 API, 페이징, 답글 표시, 신고/삭제 액션이 없다.
- 기존 프로필 FanTalk 구현은 구 API와 구 UI 구조를 사용하므로 V2 크리에이터 채널 탭에 그대로 붙이기 어렵다. - 기존 프로필 FanTalk 구현은 구 API와 구 UI 구조를 사용하므로 V2 크리에이터 채널 탭에 그대로 붙이기 어렵다.
- 팬Talk 탭의 글쓰기 floating button과 empty state `응원 남기기` 버튼이 실제 글 입력 화면으로 연결되지 않는다.
- 내가 쓴 팬Talk의 `수정하기` 액션이 실제 수정 화면으로 연결되지 않는다.
- V2 화면에서 재사용할 수 있는 Figma 기준 액션 모달이 없어 입력 종료 안내 모달을 일관되게 표시하기 어렵다.
## 3. Goals ## 3. Goals
- `CreatorChannelTab.fanTalk` 선택 시 팬Talk 탭 API를 호출하고 응답 데이터로 화면을 구성한다. - `CreatorChannelTab.fanTalk` 선택 시 팬Talk 탭 API를 호출하고 응답 데이터로 화면을 구성한다.
@@ -30,13 +40,20 @@ Figma 참조:
- `삭제하기`는 기존 FanTalk에서 사용하던 삭제 API와 동일하게 `ExplorerRepository.modifyCheers(..., isActive: false)`를 사용한다. - `삭제하기`는 기존 FanTalk에서 사용하던 삭제 API와 동일하게 `ExplorerRepository.modifyCheers(..., isActive: false)`를 사용한다.
- `fanTalkCount == 0`이면 Figma empty view(`290:9001`)를 표시한다. - `fanTalkCount == 0`이면 Figma empty view(`290:9001`)를 표시한다.
- 내 채널인 경우 우측하단 글쓰기 floating button과 empty view 내부 `응원 남기기` 버튼을 모두 표시하지 않는다. - 내 채널인 경우 우측하단 글쓰기 floating button과 empty view 내부 `응원 남기기` 버튼을 모두 표시하지 않는다.
- 크리에이터 채널 팬Talk 탭의 우측하단 plus button과 empty state `응원 남기기` 버튼에서 팬Talk 글 입력 화면으로 진입한다.
- 팬Talk 글 입력 화면은 입력 값 없음, 입력 값 있음, 키보드 없음 상태를 Figma 기준으로 표시한다.
- 입력 값이 없으면 보내기 버튼을 비활성화하고, 입력 값이 있으면 보내기 버튼을 활성화한다.
- 입력 값이 있는 상태에서 취소 버튼을 터치하면 입력 종료 안내 모달을 표시한다.
- 입력 종료 안내 모달은 V2 공용 액션 모달로 구현해 제목, 안내 문구, 버튼 1, 버튼 2의 label/action을 바꿔 재사용할 수 있게 한다.
- 팬Talk 작성과 수정은 이번에 작성하는 같은 쓰기 페이지에서 수행한다.
- 작성/수정 API는 기존 팬Talk API인 `ExplorerRepository.writeCheers``ExplorerRepository.modifyCheers`를 사용한다.
## 4. Non-Goals ## 4. Non-Goals
- 크리에이터 채널 공통 shell, header, sticky tab-bar 동작을 다시 설계하지 않는다. - 크리에이터 채널 공통 shell, header, sticky tab-bar 동작을 다시 설계하지 않는다.
- 신규 글쓰기/수정 페이지를 이번 작업에서 구현하지 않는다. - 팬Talk 작성/수정용 새 API endpoint를 만들지 않는다.
- 팬Talk 작성/수정 mutation API를 새로 만들지 않는다.
- 신고 API나 신고 dialog를 새로 만들지 않는다. - 신고 API나 신고 dialog를 새로 만들지 않는다.
- 삭제 API를 새로 만들지 않고 기존 FanTalk 삭제 흐름을 재사용한다. - 삭제 API를 새로 만들지 않고 기존 FanTalk 삭제 흐름을 재사용한다.
- 기존 구 UI `SodaDialog`를 V2 공용 모달로 대체하는 전역 리팩터링은 하지 않는다.
- Figma localhost asset URL을 앱 코드에 직접 사용하지 않는다. - Figma localhost asset URL을 앱 코드에 직접 사용하지 않는다.
- `Pods/**`, `generated/**`, `build/**`는 수정하지 않는다. - `Pods/**`, `generated/**`, `build/**`는 수정하지 않는다.
@@ -118,7 +135,7 @@ data class CreatorChannelFanTalkReplyResponse(
- 더보기 버튼을 터치하면 Figma `666:36114` 기준 anchored context popup을 표시한다. - 더보기 버튼을 터치하면 Figma `666:36114` 기준 anchored context popup을 표시한다.
- 내가 쓴 글이면 `수정하기`, `삭제하기`를 표시한다. - 내가 쓴 글이면 `수정하기`, `삭제하기`를 표시한다.
- 내가 쓴 글이 아니지만 내 채널이면 `삭제하기`만 표시한다. - 내가 쓴 글이 아니지만 내 채널이면 `삭제하기`만 표시한다.
- `수정하기`와 글쓰기 floating button은 후속 신규 글쓰기 페이지 구현 전까지 비어 있는 placeholder action으로 두되, 코드상 TODO가 드러나게 유지한다. - `수정하기`를 터치하면 이번에 작성하는 팬Talk 쓰기 페이지를 수정 모드로 연다.
- `삭제하기`는 확인 dialog 후 기존 삭제 API를 호출한다. - `삭제하기`는 확인 dialog 후 기존 삭제 API를 호출한다.
### 5.6 Report and delete ### 5.6 Report and delete
@@ -138,7 +155,7 @@ data class CreatorChannelFanTalkReplyResponse(
- 글쓰기 floating button은 `CreatorChannelFloatingActionMenu` 내부 `floatingButton`과 동일한 66pt 원형 버튼, 38pt 아이콘, `Color.soda400` 배경을 사용한다. - 글쓰기 floating button은 `CreatorChannelFloatingActionMenu` 내부 `floatingButton`과 동일한 66pt 원형 버튼, 38pt 아이콘, `Color.soda400` 배경을 사용한다.
- 동일 UI를 중복 구현하지 않기 위해 `SodaLive/Sources/V2/CreatorChannel/Components/**` 아래에 공용 icon floating button을 분리하고, `CreatorChannelFloatingActionMenu`와 팬Talk 글쓰기 floating button이 이를 재사용한다. - 동일 UI를 중복 구현하지 않기 위해 `SodaLive/Sources/V2/CreatorChannel/Components/**` 아래에 공용 icon floating button을 분리하고, `CreatorChannelFloatingActionMenu`와 팬Talk 글쓰기 floating button이 이를 재사용한다.
- 글쓰기 floating button에는 plus 아이콘만 표시하고 텍스트는 표시하지 않는다. - 글쓰기 floating button에는 plus 아이콘만 표시하고 텍스트는 표시하지 않는다.
- 글쓰기 버튼 tap action은 신규 글쓰기 페이지 구현 후 연결할 수 있도록 부모 closure로 분리한다. - 글쓰기 버튼 tap action은 이번에 작성하는 팬Talk 쓰기 페이지의 작성 모드로 연결한다.
### 5.8 Pagination ### 5.8 Pagination
- 첫 진입 시 `page=0`, `size=20`으로 조회한다. - 첫 진입 시 `page=0`, `size=20`으로 조회한다.
@@ -147,6 +164,48 @@ data class CreatorChannelFanTalkReplyResponse(
- 다음 페이지 성공 시 기존 `fanTalks` 뒤에 append한다. - 다음 페이지 성공 시 기존 `fanTalks` 뒤에 append한다.
- 첫 페이지 재조회 시 기존 목록을 교체한다. - 첫 페이지 재조회 시 기존 목록을 교체한다.
### 5.9 FanTalk write/modify entry
- 쓰기 페이지는 크리에이터 채널 팬Talk 탭에서 진입한다.
- 진입 경로는 `fanTalkCount > 0`일 때 표시되는 우측하단 plus button과 `fanTalkCount == 0`일 때 empty state에 표시되는 `응원 남기기` 버튼이다.
- 내가 쓴 글의 더보기 메뉴에서 `수정하기`를 터치하면 같은 쓰기 페이지를 수정 모드로 연다.
- 수정 모드로 진입할 때는 수정 대상 `fanTalkId`와 기존 글 content를 반드시 함께 전달한다.
- 내 채널인 경우 기존 규칙대로 plus button과 empty state `응원 남기기` 버튼을 모두 표시하지 않으므로 작성 모드로 진입할 수 없다.
- 쓰기 route는 `AppStep`에 추가하고 `creatorId`, 작성/수정 mode, 수정 대상 `fanTalkId`, 초기 content, `onSuccess`를 전달할 수 있게 한다.
- 작성 모드에서는 `fanTalkId`와 초기 content를 전달하지 않거나 비어 있는 값으로 전달한다.
- 수정 모드에서는 `fanTalkId`가 0보다 커야 하고 초기 content가 기존 글 content와 같아야 한다.
- 작성 또는 수정 성공 후 `onSuccess`로 팬Talk 첫 페이지를 다시 조회한다.
### 5.10 FanTalk write/modify screen
- 쓰기 화면은 Figma `290:9235`, `290:9282`, `290:9290`을 기준으로 한다.
- Figma `290:9235`, `290:9282`는 키보드가 올라와 있는 상태를 기준으로 한다.
- 화면 진입 시 입력 영역에 focus를 주어 키보드가 올라오게 한다.
- 작성 모드는 빈 content로 시작한다.
- 수정 모드는 선택한 fanTalk의 기존 content를 입력값으로 채운 상태에서 시작한다.
- 수정 모드의 보내기 API 호출에는 route로 받은 `fanTalkId`와 현재 입력 content를 사용한다.
- 입력 값이 없으면 보내기 버튼을 비활성화한다.
- 입력 값이 있으면 보내기 버튼을 활성화한다.
- 보내기 버튼 활성화 기준은 `content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false`이다.
- 키보드가 내려가도 입력 값과 보내기 버튼 활성화 상태는 유지한다.
- 입력 값이 없는 상태에서 취소 버튼을 터치하면 이전 화면으로 닫는다.
- 입력 값이 있는 상태에서 취소 버튼을 터치하면 입력 종료 안내 모달을 표시한다.
### 5.11 Reusable V2 action modal
- 입력 종료 안내 모달은 Figma `308:19046` 기준으로 표시한다.
- 공용 모달 컴포넌트는 Figma `567:17662`, `567:17666`, `567:17712`을 기준으로 한다.
- 공용 모달은 제목, 안내 문구, button 1, button 2를 외부에서 주입받는다.
- button 1은 오른쪽, button 2는 왼쪽에 표시한다.
- 각 버튼은 label과 action이 모두 있을 때만 표시한다.
- 버튼 1개만 표시할 수 있어야 하며, 버튼 2개 표시도 지원해야 한다.
- Dimmed는 Black, opacity 60%를 사용한다.
- Dimmed tap은 별도 action 없이 모달만 닫는다.
- 저장소 경로 관례에 맞춰 공용 모달은 `SodaLive/Sources/V2/Component/Modal/**` 아래에 둔다.
### 5.12 FanTalk write/modify submission
- 작성 API는 기존 팬Talk API인 `ExplorerRepository.writeCheers(parentCheersId:nil, creatorId:content:)`를 사용한다.
- 수정 API는 기존 팬Talk API인 `ExplorerRepository.modifyCheers(cheersId:content:isActive:)`를 사용하고, `content`에는 수정한 입력값을 전달하며 `isActive``nil`로 전달한다.
- 작성 또는 수정 성공 시 쓰기 화면을 닫고 팬Talk 탭 첫 페이지를 재조회한다.
- 작성 또는 수정 실패 시 기존 V2 화면 관례대로 toast 또는 오류 팝업을 표시한다.
## 6. Success Criteria ## 6. Success Criteria
- 팬Talk 탭 진입 시 `GET /api/v2/creator-channels/{creatorId}/fan-talks?page=0&size=20`가 호출된다. - 팬Talk 탭 진입 시 `GET /api/v2/creator-channels/{creatorId}/fan-talks?page=0&size=20`가 호출된다.
- count bar에는 `전체 {fanTalkCount}`만 표시되고 정렬 버튼/아이콘/popup은 표시되지 않는다. - count bar에는 `전체 {fanTalkCount}`만 표시되고 정렬 버튼/아이콘/popup은 표시되지 않는다.
@@ -159,20 +218,34 @@ data class CreatorChannelFanTalkReplyResponse(
- `fanTalkCount == 0`이면 Figma `290:9001` empty view가 표시된다. - `fanTalkCount == 0`이면 Figma `290:9001` empty view가 표시된다.
- 내 채널인 경우 empty view 내부 `응원 남기기` 버튼과 우측하단 글쓰기 floating button이 모두 표시되지 않는다. - 내 채널인 경우 empty view 내부 `응원 남기기` 버튼과 우측하단 글쓰기 floating button이 모두 표시되지 않는다.
- `fanTalkCount > 0`이고 내 채널이 아니면 우측하단 글쓰기 floating button이 표시된다. - `fanTalkCount > 0`이고 내 채널이 아니면 우측하단 글쓰기 floating button이 표시된다.
- 글쓰기/수정하기 tap은 신규 글쓰기 페이지 구현 전까지 앱 crash 없이 no-op placeholder로 동작한다. - `fanTalkCount > 0`이고 내 채널이 아니면 우측하단 글쓰기 floating button tap 시 팬Talk 글 입력 화면으로 이동한다.
- `fanTalkCount == 0`이고 내 채널이 아니면 empty state `응원 남기기` 버튼 tap 시 팬Talk 글 입력 화면으로 이동한다.
- 팬Talk 글 입력 화면은 입력 값이 없을 때 보내기 버튼을 비활성화한다.
- 팬Talk 글 입력 화면은 입력 값이 있을 때 보내기 버튼을 활성화한다.
- 입력 값이 있는 상태에서 취소 버튼을 터치하면 입력 종료 안내 모달이 표시된다.
- 입력 종료 안내 모달의 dimmed를 터치하면 모달만 닫히고 입력 화면과 입력 값은 유지된다.
- 작성 성공 후 팬Talk 탭 첫 페이지가 다시 조회된다.
- 내가 쓴 글의 `수정하기` tap 시 같은 팬Talk 쓰기 페이지가 수정 모드로 열린다.
- 수정 모드 진입 시 수정 대상 `fanTalkId`와 기존 글 content가 함께 전달된다.
- 수정 모드는 선택한 fanTalk의 기존 content를 입력값으로 표시한다.
- 수정 성공 후 팬Talk 탭 첫 페이지가 다시 조회된다.
## 7. Technical Constraints ## 7. Technical Constraints
- 기능 변경은 `SodaLive/Sources/V2/CreatorChannel/**` 하위에서 해결한다. - 기능 변경은 `SodaLive/Sources/V2/CreatorChannel/**` 하위에서 해결한다.
- 팬Talk 탭 전용 View, ViewModel, Repository, API, 모델은 `SodaLive/Sources/V2/CreatorChannel/FanTalk/**` 아래에 둔다. - 팬Talk 탭 전용 View, ViewModel, Repository, API, 모델은 `SodaLive/Sources/V2/CreatorChannel/FanTalk/**` 아래에 둔다.
- 우측하단 icon floating button은 팬Talk 전용 컴포넌트로 중복 생성하지 않고 `CreatorChannelFloatingActionMenu`의 private `floatingButton` 패턴을 공용 컴포넌트로 추출해 재사용한다. - 우측하단 icon floating button은 팬Talk 전용 컴포넌트로 중복 생성하지 않고 `CreatorChannelFloatingActionMenu`의 private `floatingButton` 패턴을 공용 컴포넌트로 추출해 재사용한다.
- 팬Talk 작성/수정 공용 쓰기 View와 ViewModel은 `SodaLive/Sources/V2/CreatorChannel/FanTalk/Write/**` 아래에 둔다.
- V2 공용 액션 모달은 `SodaLive/Sources/V2/Component/Modal/**` 아래에 둔다.
- 삭제 API 재사용을 위해 기존 `ExplorerRepository.modifyCheers`를 호출할 수 있다. - 삭제 API 재사용을 위해 기존 `ExplorerRepository.modifyCheers`를 호출할 수 있다.
- 작성 API 재사용을 위해 기존 `ExplorerRepository.writeCheers`를 호출한다.
- 수정 API 재사용을 위해 기존 `ExplorerRepository.modifyCheers`를 호출한다.
- 신고 기능 재사용을 위해 기존 `ReportRepository`, `ReportRequest`, `CheersReportDialogView`를 사용한다. - 신고 기능 재사용을 위해 기존 `ReportRepository`, `ReportRequest`, `CheersReportDialogView`를 사용한다.
- 신규 문구가 필요하면 `SodaLive/Sources/I18n/I18n.swift`에 ko/en/ja를 추가한다. - 신규 문구가 필요하면 `SodaLive/Sources/I18n/I18n.swift`에 ko/en/ja를 추가한다.
- 프로젝트 설정 변경은 신규 Swift 파일 target 등록이 필요한 경우에만 수행한다. - 프로젝트 설정 변경은 신규 Swift 파일 target 등록이 필요한 경우에만 수행한다.
## 8. Assumptions ## 8. Assumptions
- 신규 글쓰기/수정 페이지는 후속 작업에서 구현하며, 이번 작업에서는 버튼 표시와 action closure만 준비한다.
- 신규 API의 `fanTalkId`는 기존 신고/삭제 API가 요구하는 `cheersId`와 동일한 식별자로 사용할 수 있다. - 신규 API의 `fanTalkId`는 기존 신고/삭제 API가 요구하는 `cheersId`와 동일한 식별자로 사용할 수 있다.
- 기존 `ExplorerRepository.writeCheers``ExplorerRepository.modifyCheers`는 이번 팬Talk 작성/수정 API로 사용한다.
## 9. Open Questions ## 9. Open Questions
- 해당 없음 - 해당 없음
@@ -180,3 +253,11 @@ data class CreatorChannelFanTalkReplyResponse(
## 10. Verification Notes ## 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: Figma `get_design_context``get_screenshot`으로 `290:9139`, `290:9145`, `290:9147`, `666:36114`, `290:9001`을 확인했다.
- 2026-07-04: `CreatorChannelView`, 후원 탭 구현, 기존 `UserProfileFanTalkViewModel`, `UserProfileFanTalkCheersItemView`, `UserProfileView`의 신고/삭제 흐름을 확인했다. - 2026-07-04: `CreatorChannelView`, 후원 탭 구현, 기존 `UserProfileFanTalkViewModel`, `UserProfileFanTalkCheersItemView`, `UserProfileView`의 신고/삭제 흐름을 확인했다.
- 2026-07-06: 사용자 요청에 따라 Swift 구현 없이 PRD에 팬Talk 글 입력 화면 Figma URL(`290:9235`, `290:9282`, `290:9290`), 입력 종료 모달(`308:19046`), 공용 모달(`567:17662`, `567:17666`, `567:17712`)과 요구사항을 추가했다.
- 2026-07-06: `rg "290:9235|290:9282|290:9290|308:19046|567:17662|567:17666|567:17712|SodaV2ActionModal|ExplorerRepository.writeCheers" docs/20260704_크리에이터_채널_팬Talk_탭/prd.md docs/20260704_크리에이터_채널_팬Talk_탭/plan-task.md`로 Figma node, 공용 모달, 기존 작성 API 재사용 방침이 문서에 포함된 것을 확인했다.
- 2026-07-06: `git diff --check -- docs/20260704_크리에이터_채널_팬Talk_탭/prd.md docs/20260704_크리에이터_채널_팬Talk_탭/plan-task.md` 실행 결과 출력 없이 종료되어 공백 오류가 없음을 확인했다.
- 2026-07-06: 사용자 요청에 따라 팬Talk 작성/수정 모두 같은 쓰기 페이지에서 수행하고, 작성/수정 API는 기존 팬Talk API(`ExplorerRepository.writeCheers`, `ExplorerRepository.modifyCheers`)를 사용하도록 PRD를 보완했다.
- 2026-07-06: `rg "작성/수정|쓰기 페이지|수정 모드|작성 모드|ExplorerRepository.writeCheers|ExplorerRepository.modifyCheers|modifyCheers\\(cheersId:fanTalkId, content:content, isActive:nil\\)|creatorChannelFanTalkWrite|showFanTalkModifyView" docs/20260704_크리에이터_채널_팬Talk_탭/prd.md docs/20260704_크리에이터_채널_팬Talk_탭/plan-task.md`로 작성/수정 공용 쓰기 페이지와 기존 팬Talk API 사용 방침을 확인했다.
- 2026-07-06: `rg -n "no-op|수정 페이지는|수정 mutation|작성용 새 API|재사용 가능 여부|불가능한 것이 확인|미결정 사항|수정하기 tap은 수정 페이지" docs/20260704_크리에이터_채널_팬Talk_탭/prd.md docs/20260704_크리에이터_채널_팬Talk_탭/plan-task.md` 실행 결과 매칭이 없어 이전 충돌 문구가 남지 않았음을 확인했다.
- 2026-07-06: 사용자 확인에 따라 수정 모드 진입 시 수정 대상 `fanTalkId`와 기존 글 content를 반드시 함께 전달하도록 PRD를 보완했다.
- 2026-07-06: `rg '수정 모드.*fanTalkId|fanTalkId.*기존 글 content|fanTalkId.*초기 content|fanTalkId.*initialContent|기존 content.*content 초기값|수정 대상.*fanTalkId.*기존 글 content|fanTalkId.*0보다 커야' docs/20260704_크리에이터_채널_팬Talk_탭/prd.md docs/20260704_크리에이터_채널_팬Talk_탭/plan-task.md`로 수정 모드 route에 `fanTalkId`와 기존 글 content를 함께 전달한다는 조건을 확인했다.