From a8fe416cb59d02e9b35f697b5db4499cb02ea25e Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Fri, 31 Jul 2026 21:12:32 +0900 Subject: [PATCH] =?UTF-8?q?fix(main):=20=EC=B6=94=EC=B2=9C=20=ED=99=88=20?= =?UTF-8?q?=ED=9B=84=EC=86=8D=20=EA=B2=80=EC=A6=9D=EC=9D=84=20=EB=B3=B4?= =?UTF-8?q?=EC=99=84=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SodaLive.xcodeproj/project.pbxproj | 52 +- SodaLive/Sources/I18n/I18n.swift | 18 +- .../Sources/Live/Now/All/LiveNowAllView.swift | 2 +- .../V2/Component/Banner/BannerCarousel.swift | 11 + .../V2/Component/Card/CommunityPostCard.swift | 44 +- ...tView.swift => V2ExpandableTextView.swift} | 6 +- .../CreatorChannel/CreatorChannelView.swift | 2 +- .../Sources/V2/Main/Home/MainHomeView.swift | 6 +- .../MainHomeActiveCreatorSection.swift | 36 +- .../MainHomeAiCharacterSection.swift | 2 +- .../MainHomeBusinessInfoSection.swift | 53 +- .../MainHomeRecommendationViewModel.swift | 1 + .../MainHomeRecommendationResponse.swift | 12 - SodaLive/Sources/V2/Main/MainView.swift | 56 +- .../plan-task.md | 870 +++++++++++++++++- docs/20260602_메인_홈_추천_UI_API_연동/prd.md | 63 +- .../phase1-spec-and-existing-implementation.md | 64 ++ .../reviews/phase10-active-creators.md | 88 ++ .../reviews/phase11-recent-debut-creators.md | 82 ++ .../reviews/phase12-ai-character-section.md | 91 ++ .../reviews/phase13-genre-creators.md | 63 ++ .../reviews/phase14-cheer-creators.md | 61 ++ .../reviews/phase15-popular-community-feed.md | 121 +++ .../reviews/phase16-business-info-section.md | 123 +++ .../reviews/phase17-section-assembly.md | 66 ++ .../reviews/phase18-final-project-build.md | 158 ++++ .../reviews/phase19-documentation-records.md | 173 ++++ .../phase2-recommendation-api-response-model.md | 103 +++ .../reviews/phase20-community-post-routing.md | 62 ++ .../reviews/phase21-recent-activity-live-fallback.md | 27 + .../reviews/phase22-follow-all-confirmation.md | 77 ++ .../reviews/phase3-activity-type-i18n.md | 62 ++ .../reviews/phase4-recommendation-state-layer.md | 99 ++ .../reviews/phase5-detail-routing-guards.md | 226 +++++ .../reviews/phase6-home-tab-shell.md | 82 ++ .../reviews/phase7-intermediate-project-build.md | 79 ++ .../reviews/phase8-live-section.md | 61 ++ .../reviews/phase9-banner-section.md | 122 +++ 38 files changed, 3161 insertions(+), 163 deletions(-) rename SodaLive/Sources/V2/Component/Text/{ExpandableTextView.swift => V2ExpandableTextView.swift} (88%) create mode 100644 docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase1-spec-and-existing-implementation.md create mode 100644 docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase10-active-creators.md create mode 100644 docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase11-recent-debut-creators.md create mode 100644 docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase12-ai-character-section.md create mode 100644 docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase13-genre-creators.md create mode 100644 docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase14-cheer-creators.md create mode 100644 docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase15-popular-community-feed.md create mode 100644 docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase16-business-info-section.md create mode 100644 docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase17-section-assembly.md create mode 100644 docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase18-final-project-build.md create mode 100644 docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase19-documentation-records.md create mode 100644 docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase2-recommendation-api-response-model.md create mode 100644 docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase20-community-post-routing.md create mode 100644 docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase22-follow-all-confirmation.md create mode 100644 docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase3-activity-type-i18n.md create mode 100644 docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase4-recommendation-state-layer.md create mode 100644 docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase5-detail-routing-guards.md create mode 100644 docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase6-home-tab-shell.md create mode 100644 docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase7-intermediate-project-build.md create mode 100644 docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase8-live-section.md create mode 100644 docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase9-banner-section.md diff --git a/SodaLive.xcodeproj/project.pbxproj b/SodaLive.xcodeproj/project.pbxproj index b4893f88..5cf9ec07 100644 --- a/SodaLive.xcodeproj/project.pbxproj +++ b/SodaLive.xcodeproj/project.pbxproj @@ -213,29 +213,20 @@ 401A3FA92FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB52FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift */; }; 401A3FAA2FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB62FEEAFA000A5BBA3 /* MainHomeRankingView.swift */; }; 401A3FAB2FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB72FEEAFA000A5BBA3 /* MainHomeFollowingView.swift */; }; - 401A3FAC2FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB42FEEAFA000A5BBA3 /* MainHomeView.swift */; }; - 401A3FAD2FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB52FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift */; }; - 401A3FAE2FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB62FEEAFA000A5BBA3 /* MainHomeRankingView.swift */; }; - 401A3FAF2FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB72FEEAFA000A5BBA3 /* MainHomeFollowingView.swift */; }; 401A3FB02FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB42FEEAFA000A5BBA3 /* MainHomeView.swift */; }; 401A3FB12FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB52FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift */; }; 401A3FB22FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB62FEEAFA000A5BBA3 /* MainHomeRankingView.swift */; }; 401A3FB32FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB72FEEAFA000A5BBA3 /* MainHomeFollowingView.swift */; }; 401A3FBA2FEEAFA000A5BBA3 /* MainHomeRecommendationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76506E56D017236824FE9B8F /* MainHomeRecommendationViewModel.swift */; }; 401A3FC22FEEB00100A5BBA3 /* MainHomeLiveSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FC12FEEB00100A5BBA3 /* MainHomeLiveSection.swift */; }; - 401A3FC32FEEB00100A5BBA3 /* MainHomeLiveSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FC12FEEB00100A5BBA3 /* MainHomeLiveSection.swift */; }; 401A3FC42FEEB00100A5BBA3 /* MainHomeLiveSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FC12FEEB00100A5BBA3 /* MainHomeLiveSection.swift */; }; 401A3FC62FEEB00200A5BBA3 /* MainHomeBannerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FC52FEEB00200A5BBA3 /* MainHomeBannerSection.swift */; }; - 401A3FC72FEEB00200A5BBA3 /* MainHomeBannerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FC52FEEB00200A5BBA3 /* MainHomeBannerSection.swift */; }; 401A3FC82FEEB00200A5BBA3 /* MainHomeBannerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FC52FEEB00200A5BBA3 /* MainHomeBannerSection.swift */; }; 401A3FC92FEEB00300A5BBA3 /* MainHomeActiveCreatorSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FCC2FEEB00300A5BBA3 /* MainHomeActiveCreatorSection.swift */; }; - 401A3FCA2FEEB00300A5BBA3 /* MainHomeActiveCreatorSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FCC2FEEB00300A5BBA3 /* MainHomeActiveCreatorSection.swift */; }; 401A3FCB2FEEB00300A5BBA3 /* MainHomeActiveCreatorSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FCC2FEEB00300A5BBA3 /* MainHomeActiveCreatorSection.swift */; }; 401A3FCE2FEEB00400A5BBA3 /* MainHomeRecentDebutCreatorSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FCD2FEEB00400A5BBA3 /* MainHomeRecentDebutCreatorSection.swift */; }; - 401A3FCF2FEEB00400A5BBA3 /* MainHomeRecentDebutCreatorSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FCD2FEEB00400A5BBA3 /* MainHomeRecentDebutCreatorSection.swift */; }; 401A3FD02FEEB00400A5BBA3 /* MainHomeRecentDebutCreatorSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FCD2FEEB00400A5BBA3 /* MainHomeRecentDebutCreatorSection.swift */; }; 401A3FD12FEEB00500A5BBA3 /* MainHomeAiCharacterSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FD42FEEB00500A5BBA3 /* MainHomeAiCharacterSection.swift */; }; - 401A3FD22FEEB00500A5BBA3 /* MainHomeAiCharacterSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FD42FEEB00500A5BBA3 /* MainHomeAiCharacterSection.swift */; }; 401A3FD32FEEB00500A5BBA3 /* MainHomeAiCharacterSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FD42FEEB00500A5BBA3 /* MainHomeAiCharacterSection.swift */; }; 401A3FD72FEEB00600A5BBA3 /* MainHomeGenreCreatorSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FD52FEEB00600A5BBA3 /* MainHomeGenreCreatorSection.swift */; }; 401A3FD82FEEB00600A5BBA3 /* MainHomeGenreCreatorSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FD52FEEB00600A5BBA3 /* MainHomeGenreCreatorSection.swift */; }; @@ -244,7 +235,6 @@ 401A3FDD2FEEB00800A5BBA3 /* MainHomePopularCommunitySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FDC2FEEB00800A5BBA3 /* MainHomePopularCommunitySection.swift */; }; 401A3FDE2FEEB00800A5BBA3 /* MainHomePopularCommunitySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FDC2FEEB00800A5BBA3 /* MainHomePopularCommunitySection.swift */; }; 401A3FE02FEEB00900A5BBA3 /* MainHomeBusinessInfoSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FDF2FEEB00900A5BBA3 /* MainHomeBusinessInfoSection.swift */; }; - 401A3FE12FEEB00900A5BBA3 /* MainHomeBusinessInfoSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FDF2FEEB00900A5BBA3 /* MainHomeBusinessInfoSection.swift */; }; 401A3FE22FEEB00900A5BBA3 /* MainHomeBusinessInfoSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FDF2FEEB00900A5BBA3 /* MainHomeBusinessInfoSection.swift */; }; 401B3F012FEEF10000A5BBA3 /* MainHomeCreatorRankingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401B3F0A2FEEF10000A5BBA3 /* MainHomeCreatorRankingResponse.swift */; }; 401B3F022FEEF10000A5BBA3 /* MainHomeRankingApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401B3F0B2FEEF10000A5BBA3 /* MainHomeRankingApi.swift */; }; @@ -409,6 +399,8 @@ 405F792A2D2BA4040037DE3A /* AuditionDetailRoleItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405F79282D2BA4040037DE3A /* AuditionDetailRoleItemView.swift */; }; 405F792C2D2BCA790037DE3A /* ExpandableTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405F792B2D2BCA790037DE3A /* ExpandableTextView.swift */; }; 405F792D2D2BCA790037DE3A /* ExpandableTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405F792B2D2BCA790037DE3A /* ExpandableTextView.swift */; }; + C922F89B79C84E542AD7DE4F /* V2ExpandableTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C922F89A79C84E542AD7DE4F /* V2ExpandableTextView.swift */; }; + C922F89C79C84E542AD7DE4F /* V2ExpandableTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C922F89A79C84E542AD7DE4F /* V2ExpandableTextView.swift */; }; 405F792F2D2BDABC0037DE3A /* AuditionRoleDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405F792E2D2BDABC0037DE3A /* AuditionRoleDetailView.swift */; }; 405F79302D2BDABC0037DE3A /* AuditionRoleDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405F792E2D2BDABC0037DE3A /* AuditionRoleDetailView.swift */; }; 405F79322D2BDFD50037DE3A /* AuditionRoleDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 405F79312D2BDFD50037DE3A /* AuditionRoleDetailViewModel.swift */; }; @@ -1675,7 +1667,6 @@ A376733CDBF21EBAF81C8BE2 /* SocialLoginRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0050039EBD24D2FF6D0BD0 /* SocialLoginRequest.swift */; }; A3BDD0D3E14CD72B022AF6A7 /* FollowButtonImageAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC033E531A9420363D017CE /* FollowButtonImageAsset.swift */; }; A3EB81FD727C63FE034048B9 /* Spacing.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B18C00DE256DCC53030630 /* Spacing.swift */; }; - A4EB669DFFFB64681E045EF3 /* MainHomeRecommendationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76506E56D017236824FE9B8F /* MainHomeRecommendationViewModel.swift */; }; A840136779D6CC95F94CB681 /* HomeTitleBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAA81E8424A575A0F8546D31 /* HomeTitleBar.swift */; }; ACD71056E97E13E48941ABBB /* MainPlaceholderTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7900EA9FD1150BABE64CF1B3 /* MainPlaceholderTabView.swift */; }; AD6424211AA898794BBE8470 /* SectionTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16C12DFB83FE164B38CC066 /* SectionTitle.swift */; }; @@ -1687,7 +1678,7 @@ B11681AC02BF51C6E2FA6FF3 /* MainTabBarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F49E8BB9BACFCA8878B70D /* MainTabBarButton.swift */; }; B3AF650899B8FC84E0C0CDA0 /* MainPlaceholderTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7900EA9FD1150BABE64CF1B3 /* MainPlaceholderTabView.swift */; }; B5E9FA82CF4238F640702D4A /* CreatorDetailDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7406AE69F7667FAA2E6DD23 /* CreatorDetailDialogView.swift */; }; - BA2B76FDCAF81D15E2D4E258 /* MainHomeRecommendationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76506E56D017236824FE9B8F /* MainHomeRecommendationViewModel.swift */; }; + A4EB669DFFFB64681E045EF3 /* MainHomeRecommendationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76506E56D017236824FE9B8F /* MainHomeRecommendationViewModel.swift */; }; BA511503C1E037162697D771 /* DonationRankingPeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44A42B63A1021DD9F8AA3F30 /* DonationRankingPeriod.swift */; }; BCA6CD9E57528C5F4203F1A5 /* CapsuleTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74990A0798E64A97237DD113 /* CapsuleTabBar.swift */; }; BEAAA523CAD3198065A557BC /* EntityInfo-SodaLive-dev.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0ED95AC08C6481751AE9D97 /* EntityInfo-SodaLive-dev.generated.swift */; }; @@ -3008,7 +2999,7 @@ C1165E3F4CA1AB019FC85067 /* LanguageRepository.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LanguageRepository.swift; sourceTree = ""; }; C40163E3E9DB325EA984D3C4 /* CreatorChannelFloatingIconButton.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreatorChannelFloatingIconButton.swift; sourceTree = ""; }; C6B18C00DE256DCC53030630 /* Spacing.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Spacing.swift; sourceTree = ""; }; - C922F89A79C84E542AD7DE4F /* ExpandableTextView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExpandableTextView.swift; sourceTree = ""; }; + C922F89A79C84E542AD7DE4F /* V2ExpandableTextView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = V2ExpandableTextView.swift; sourceTree = ""; }; CC0100012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelHomeResponse.swift; sourceTree = ""; }; CC0100032FF0100000A5BBA3 /* CreatorChannelHomeApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelHomeApi.swift; sourceTree = ""; }; CC0100042FF0100000A5BBA3 /* CreatorChannelHomeRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelHomeRepository.swift; sourceTree = ""; }; @@ -3221,7 +3212,7 @@ 0FD267162A809D893E2E3B3F /* Text */ = { isa = PBXGroup; children = ( - C922F89A79C84E542AD7DE4F /* ExpandableTextView.swift */, + C922F89A79C84E542AD7DE4F /* V2ExpandableTextView.swift */, ); path = Text; sourceTree = ""; @@ -6840,6 +6831,7 @@ 401A3FCE2FEEB00400A5BBA3 /* MainHomeRecentDebutCreatorSection.swift in Sources */, 401A3FD12FEEB00500A5BBA3 /* MainHomeAiCharacterSection.swift in Sources */, 401A3FE02FEEB00900A5BBA3 /* MainHomeBusinessInfoSection.swift in Sources */, + C922F89B79C84E542AD7DE4F /* V2ExpandableTextView.swift in Sources */, C0A704000000000000000003 /* AudioContentListRow.swift in Sources */, C0A711000000000000000001 /* MainContentAllTabResponse.swift in Sources */, C0A711000000000000000002 /* MainContentAllApi.swift in Sources */, @@ -7654,7 +7646,6 @@ AD6424211AA898794BBE8470 /* SectionTitle.swift in Sources */, 38D73A7B6BFB9A984834984B /* TextTabBar.swift in Sources */, 372837FE9857C16B6B5087FF /* TitleBar.swift in Sources */, - BA2B76FDCAF81D15E2D4E258 /* MainHomeRecommendationViewModel.swift in Sources */, CC0102012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift in Sources */, CC0102032FF0100000A5BBA3 /* CreatorChannelHomeApi.swift in Sources */, CC0102042FF0100000A5BBA3 /* CreatorChannelHomeRepository.swift in Sources */, @@ -7742,17 +7733,9 @@ CE93595BF7A2BF7301F517DB /* CreatorChannelFanTalkWriteView.swift in Sources */, CE93595CF7A2BF7301F517DB /* SodaV2ActionModal.swift in Sources */, 401B3F092FEEF10000A5BBA3 /* MainHomeRankingViewModel.swift in Sources */, - 401A3FAC2FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */, - 401A3FAD2FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */, - 401A3FC32FEEB00100A5BBA3 /* MainHomeLiveSection.swift in Sources */, - 401A3FC72FEEB00200A5BBA3 /* MainHomeBannerSection.swift in Sources */, - 401A3FCA2FEEB00300A5BBA3 /* MainHomeActiveCreatorSection.swift in Sources */, - 401A3FCF2FEEB00400A5BBA3 /* MainHomeRecentDebutCreatorSection.swift in Sources */, - 401A3FD22FEEB00500A5BBA3 /* MainHomeAiCharacterSection.swift in Sources */, 401A3FD72FEEB00600A5BBA3 /* MainHomeGenreCreatorSection.swift in Sources */, 401A3FD92FEEB00700A5BBA3 /* MainHomeCheerCreatorSection.swift in Sources */, 401A3FDD2FEEB00800A5BBA3 /* MainHomePopularCommunitySection.swift in Sources */, - 401A3FE12FEEB00900A5BBA3 /* MainHomeBusinessInfoSection.swift in Sources */, C0A712000000000000000001 /* MainContentAllTabResponse.swift in Sources */, C0A712000000000000000002 /* MainContentAllApi.swift in Sources */, C0A712000000000000000003 /* MainContentAllRepository.swift in Sources */, @@ -7796,8 +7779,6 @@ C0A72200000000000000000A /* MainContentAudioRankingThumbnailCard.swift in Sources */, C0A722000000000000000009 /* MainContentAudioRankingEmptyStateView.swift in Sources */, C0A706000000000000000001 /* AudioContentThumbnailCard.swift in Sources */, - 401A3FAE2FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */, - 401A3FAF2FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */, 401C10212FEF300000A5BBA3 /* HomeFollowingTabResponse.swift in Sources */, 401C10222FEF300000A5BBA3 /* MainHomeFollowingApi.swift in Sources */, 401C10232FEF300000A5BBA3 /* MainHomeFollowingRepository.swift in Sources */, @@ -8019,6 +8000,7 @@ 40C1B6012BDFECB8008F6810 /* SeriesKeywordChipView.swift in Sources */, 409144352A85D6A400EEBFB1 /* GetFollowerListResponse.swift in Sources */, 405F792D2D2BCA790037DE3A /* ExpandableTextView.swift in Sources */, + C922F89C79C84E542AD7DE4F /* V2ExpandableTextView.swift in Sources */, 40ED907B2E616C7D0097BB4E /* CharacterView.swift in Sources */, 40B93F832A83C05900AE29E2 /* GetRecommendChannelResponse.swift in Sources */, 40728D582D79A66300D2C580 /* AppViewModel.swift in Sources */, @@ -8600,6 +8582,16 @@ 7A1DDAAE227BF176BDEF1CB9 /* TextTabBar.swift in Sources */, 5EEE98FE0EC4039761C604EE /* TitleBar.swift in Sources */, A4EB669DFFFB64681E045EF3 /* MainHomeRecommendationViewModel.swift in Sources */, + 401A3FB02FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */, + 401A3FB12FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */, + 401A3FC42FEEB00100A5BBA3 /* MainHomeLiveSection.swift in Sources */, + 401A3FC82FEEB00200A5BBA3 /* MainHomeBannerSection.swift in Sources */, + 401A3FCB2FEEB00300A5BBA3 /* MainHomeActiveCreatorSection.swift in Sources */, + 401A3FD02FEEB00400A5BBA3 /* MainHomeRecentDebutCreatorSection.swift in Sources */, + 401A3FD32FEEB00500A5BBA3 /* MainHomeAiCharacterSection.swift in Sources */, + 401A3FE22FEEB00900A5BBA3 /* MainHomeBusinessInfoSection.swift in Sources */, + 401A3FB22FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */, + 401A3FB32FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */, CC0103012FF0100000A5BBA3 /* CreatorChannelHomeResponse.swift in Sources */, CC0103032FF0100000A5BBA3 /* CreatorChannelHomeApi.swift in Sources */, CC0103042FF0100000A5BBA3 /* CreatorChannelHomeRepository.swift in Sources */, @@ -8684,17 +8676,9 @@ CC0630092FF0600000A5BBA3 /* CreatorChannelCommunityUploadButton.swift in Sources */, CC06300A2FF0600000A5BBA3 /* CreatorChannelCommunityEmptyView.swift in Sources */, 401B3F082FEEF10000A5BBA3 /* MainHomeRankingViewModel.swift in Sources */, - 401A3FB02FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */, - 401A3FB12FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */, - 401A3FC42FEEB00100A5BBA3 /* MainHomeLiveSection.swift in Sources */, - 401A3FC82FEEB00200A5BBA3 /* MainHomeBannerSection.swift in Sources */, - 401A3FCB2FEEB00300A5BBA3 /* MainHomeActiveCreatorSection.swift in Sources */, - 401A3FD02FEEB00400A5BBA3 /* MainHomeRecentDebutCreatorSection.swift in Sources */, - 401A3FD32FEEB00500A5BBA3 /* MainHomeAiCharacterSection.swift in Sources */, 401A3FD82FEEB00600A5BBA3 /* MainHomeGenreCreatorSection.swift in Sources */, 401A3FDA2FEEB00700A5BBA3 /* MainHomeCheerCreatorSection.swift in Sources */, 401A3FDE2FEEB00800A5BBA3 /* MainHomePopularCommunitySection.swift in Sources */, - 401A3FE22FEEB00900A5BBA3 /* MainHomeBusinessInfoSection.swift in Sources */, C0A713000000000000000001 /* MainContentAllTabResponse.swift in Sources */, C0A713000000000000000002 /* MainContentAllApi.swift in Sources */, C0A713000000000000000003 /* MainContentAllRepository.swift in Sources */, @@ -8748,8 +8732,6 @@ C0A72300000000000000000A /* MainContentAudioRankingThumbnailCard.swift in Sources */, C0A723000000000000000009 /* MainContentAudioRankingEmptyStateView.swift in Sources */, C0A706000000000000000002 /* AudioContentThumbnailCard.swift in Sources */, - 401A3FB22FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */, - 401A3FB32FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */, 401C10312FEF300000A5BBA3 /* HomeFollowingTabResponse.swift in Sources */, 401C10322FEF300000A5BBA3 /* MainHomeFollowingApi.swift in Sources */, 401C10332FEF300000A5BBA3 /* MainHomeFollowingRepository.swift in Sources */, diff --git a/SodaLive/Sources/I18n/I18n.swift b/SodaLive/Sources/I18n/I18n.swift index 472bdc3a..7ed2dc23 100644 --- a/SodaLive/Sources/I18n/I18n.swift +++ b/SodaLive/Sources/I18n/I18n.swift @@ -453,9 +453,9 @@ enum I18n { static var pinLimitDesc: String { pick( - ko: "이 콘텐츠를 고정하시겠어요? 채널에 콘텐츠를 최대 3개까지 고정할 수 있습니다. 이 콘텐츠를 고정하면 가장 오래된 콘텐츠가 대체됩니다.", - en: "Do you want to pin this content? You can pin up to 3 contents in a channel. Pinning this will replace the oldest pinned content.", - ja: "このコンテンツを固定しますか?チャンネルには最大3件まで固定できます。このコンテンツを固定すると最も古い固定コンテンツが置き換えられます。" + ko: "이 콘텐츠를 고정하시겠어요? 채널에 콘텐츠를 최대 9개까지 고정할 수 있습니다. 이 콘텐츠를 고정하면 가장 오래된 콘텐츠가 대체됩니다.", + en: "Do you want to pin this content? You can pin up to 9 contents in a channel. Pinning this will replace the oldest pinned content.", + ja: "このコンテンツを固定しますか?チャンネルには最大9件まで固定できます。このコンテンツを固定すると最も古い固定コンテンツが置き換えられます。" ) } @@ -3637,6 +3637,18 @@ If you block this user, the following features will be restricted. pick(ko: "모두 팔로우 완료", en: "All followed", ja: "すべてフォロー済み") } + static var followAllConfirmTitle: String { + pick(ko: "모두 팔로우할까요?", en: "Follow all creators?", ja: "すべてのクリエイターをフォローしますか?") + } + + static var followAllConfirmDescription: String { + pick(ko: "추천된 크리에이터를 모두 팔로우합니다.", en: "You’ll follow all recommended creators.", ja: "おすすめのクリエイターをすべてフォローします。") + } + + static var followAllConfirmAction: String { + pick(ko: "팔로우 하기", en: "Follow", ja: "フォローする") + } + static var more: String { pick(ko: "더보기", en: "More", ja: "もっと見る") } diff --git a/SodaLive/Sources/Live/Now/All/LiveNowAllView.swift b/SodaLive/Sources/Live/Now/All/LiveNowAllView.swift index 7c360a59..7446f13b 100644 --- a/SodaLive/Sources/Live/Now/All/LiveNowAllView.swift +++ b/SodaLive/Sources/Live/Now/All/LiveNowAllView.swift @@ -177,7 +177,7 @@ struct LiveNowAllView: View { let isKoreanCountry = normalizedCountryCode.isEmpty || normalizedCountryCode == "KR" if isKoreanCountry && auth == false { - pendingAction = { openLiveDetail(roomId: roomId) } + pendingAction = { handleLiveNowItemTap(roomId: roomId, isAdult: isAdult) } isShowAuthConfirmView = true return } diff --git a/SodaLive/Sources/V2/Component/Banner/BannerCarousel.swift b/SodaLive/Sources/V2/Component/Banner/BannerCarousel.swift index c092ee45..140dcfe4 100644 --- a/SodaLive/Sources/V2/Component/Banner/BannerCarousel.swift +++ b/SodaLive/Sources/V2/Component/Banner/BannerCarousel.swift @@ -22,6 +22,7 @@ struct BannerCarousel: View { @GestureState private var dragOffset: CGFloat = 0 @State private var autoScrollTimer: Timer? @State private var isSwiping = false + @State private var itemsRevision = 0 init( items: [BannerCarouselItem], @@ -70,6 +71,7 @@ struct BannerCarousel: View { .contentShape(Rectangle()) .simultaneousGesture(dragGesture(step: step)) .onChange(of: items) { _ in + itemsRevision += 1 displayIndex = items.count > 1 ? 1 : 0 restartAutoScrollTimer() } @@ -179,8 +181,15 @@ struct BannerCarousel: View { private func wrapIfNeeded() { guard items.count > 1 else { return } + displayIndex = min(max(displayIndex, 0), items.count + 1) + + let scheduledIndex = displayIndex + let scheduledRevision = itemsRevision + if displayIndex == 0 { DispatchQueue.main.asyncAfter(deadline: .now() + BannerCarousel.transitionDuration) { + guard displayIndex == scheduledIndex, itemsRevision == scheduledRevision else { return } + var transaction = Transaction() transaction.disablesAnimations = true withTransaction(transaction) { @@ -189,6 +198,8 @@ struct BannerCarousel: View { } } else if displayIndex == items.count + 1 { DispatchQueue.main.asyncAfter(deadline: .now() + BannerCarousel.transitionDuration) { + guard displayIndex == scheduledIndex, itemsRevision == scheduledRevision else { return } + var transaction = Transaction() transaction.disablesAnimations = true withTransaction(transaction) { diff --git a/SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift b/SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift index 13e3c5eb..de107d9b 100644 --- a/SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift +++ b/SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift @@ -69,6 +69,8 @@ struct CommunityPostCard: View { if hasImage { imageContent + } else if isLocked { + lockedTextPurchaseButton } if isAccessiblePost { @@ -172,6 +174,31 @@ struct CommunityPostCard: View { .buttonStyle(.plain) } + private var lockedTextPurchaseButton: some View { + Button(action: onTapPurchase) { + HStack(spacing: SodaSpacing.s6) { + Image("ic_new_community_lock") + .resizable() + .renderingMode(.template) + .foregroundColor(.white) + .frame(width: 18, height: 18) + + Image("ic_bar_cash") + .resizable() + .frame(width: 20, height: 20) + + Text("\(price)") + .appFont(.body2) + .foregroundColor(.black) + } + .padding(.vertical, SodaSpacing.s8) + .padding(.horizontal, SodaSpacing.s12) + .background(Color.white) + .clipShape(Capsule()) + } + .buttonStyle(.plain) + } + private var audioPlayButton: some View { Button { guard let audioUrl, !audioUrl.isEmpty else { return } @@ -232,7 +259,7 @@ struct CommunityPostCard: View { } private var isAccessiblePost: Bool { - price == 0 || existOrdered + price <= 0 || existOrdered } private func toggleLike() { @@ -293,6 +320,21 @@ struct CommunityPostCard_Previews: PreviewProvider { likeCount: 5, commentCount: 6 ) + + CommunityPostCard( + postId: 4, + creatorNickname: "크리에이터 이름", + creatorProfileImageUrl: previewImageUrl, + content: "이미지 없는 유료 커뮤니티 게시글입니다.", + imageUrl: nil, + audioUrl: nil, + price: 30, + existOrdered: false, + createdAt: "2분 전", + isLike: false, + likeCount: 5, + commentCount: 6 + ) } .padding(SodaSpacing.s14) .background(Color.black) diff --git a/SodaLive/Sources/V2/Component/Text/ExpandableTextView.swift b/SodaLive/Sources/V2/Component/Text/V2ExpandableTextView.swift similarity index 88% rename from SodaLive/Sources/V2/Component/Text/ExpandableTextView.swift rename to SodaLive/Sources/V2/Component/Text/V2ExpandableTextView.swift index ab6fe625..ea4b5928 100644 --- a/SodaLive/Sources/V2/Component/Text/ExpandableTextView.swift +++ b/SodaLive/Sources/V2/Component/Text/V2ExpandableTextView.swift @@ -1,6 +1,6 @@ import SwiftUI -struct ExpandableTextView: View { +struct V2ExpandableTextView: View { let text: String let lineLimit: Int let moreTitle: String @@ -71,9 +71,9 @@ struct ExpandableTextView: View { } } -struct ExpandableTextView_Previews: PreviewProvider { +struct V2ExpandableTextView_Previews: PreviewProvider { static var previews: some View { - ExpandableTextView(text: "긴 사업자 정보 텍스트가 들어가는 영역입니다. 세 줄을 넘어가면 더보기 버튼이 표시되고, 더보기 버튼을 누르면 전체 문구가 표시됩니다.") + V2ExpandableTextView(text: "긴 사업자 정보 텍스트가 들어가는 영역입니다. 세 줄을 넘어가면 더보기 버튼이 표시되고, 더보기 버튼을 누르면 전체 문구가 표시됩니다.") .padding(SodaSpacing.s20) .background(Color.black) .previewLayout(.sizeThatFits) diff --git a/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift b/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift index 947df57b..84d72535 100644 --- a/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift +++ b/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift @@ -534,7 +534,7 @@ struct CreatorChannelView: View { if isKoreanCountry && auth == false { pendingAction = { - startChat(characterId: characterId) + handleTalkTap() } isShowAuthConfirmView = true return diff --git a/SodaLive/Sources/V2/Main/Home/MainHomeView.swift b/SodaLive/Sources/V2/Main/Home/MainHomeView.swift index 133cea42..1b6e1486 100644 --- a/SodaLive/Sources/V2/Main/Home/MainHomeView.swift +++ b/SodaLive/Sources/V2/Main/Home/MainHomeView.swift @@ -101,11 +101,11 @@ private enum MainHomeTab: CaseIterable, Hashable { var title: String { switch self { case .recommendation: - return "추천" + return I18n.MainContentRecommendation.tabTitle case .ranking: - return "랭킹" + return I18n.MainContentRanking.tabTitle case .following: - return "팔로잉" + return I18n.HomeFollowing.tabTitle } } } diff --git a/SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeActiveCreatorSection.swift b/SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeActiveCreatorSection.swift index 12ceaa35..2ca24979 100644 --- a/SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeActiveCreatorSection.swift +++ b/SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeActiveCreatorSection.swift @@ -87,7 +87,7 @@ private struct MainHomeActiveCreatorItemView: View { .clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s4, style: .continuous)) } - Text(item.relativeActivityAtText()) + Text(DateParser.relativeTimeText(fromUTC: item.activityAt, fallback: item.activityAt)) .appFont(.body4) .foregroundColor(Color.gray500) .lineLimit(1) @@ -103,40 +103,6 @@ private struct MainHomeActiveCreatorItemView: View { } } -private extension HomeActiveCreatorItem { - func relativeActivityAtText(now: Date = Date()) -> String { - guard let activityDate = DateParser.parse(activityAt) else { - return activityAt - } - - let interval = max(0, now.timeIntervalSince(activityDate)) - let calendar = Calendar.current - let yearMonth = calendar.dateComponents([.year, .month], from: activityDate, to: now) - - if let years = yearMonth.year, years >= 1 { - return I18n.Time.yearsAgo(years) - } - - if let months = yearMonth.month, months >= 1 { - return I18n.Time.monthsAgo(months) - } - - if interval < 60 { - return I18n.Time.justNow - } - - if interval < 3600 { - return I18n.Time.minutesAgo(max(1, Int(interval / 60))) - } - - if interval < 86_400 { - return I18n.Time.hoursAgo(max(1, Int(interval / 3600))) - } - - return I18n.Time.daysAgo(max(1, Int(interval / 86_400))) - } -} - struct MainHomeActiveCreatorSection_Previews: PreviewProvider { private static let previewImageUrl = "https://picsum.photos/500/500" diff --git a/SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeAiCharacterSection.swift b/SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeAiCharacterSection.swift index f792b2e8..382a71b4 100644 --- a/SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeAiCharacterSection.swift +++ b/SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeAiCharacterSection.swift @@ -21,7 +21,7 @@ struct MainHomeAiCharacterSection: View { LazyHStack(alignment: .center, spacing: SodaSpacing.s4) { ForEach(items, id: \.self) { item in Button { - onTapCharacter(item.creatorId) + onTapCharacter(item.characterId) } label: { AiCharacterCard( name: item.name, diff --git a/SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeBusinessInfoSection.swift b/SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeBusinessInfoSection.swift index 8ac10cd3..f642babf 100644 --- a/SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeBusinessInfoSection.swift +++ b/SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeBusinessInfoSection.swift @@ -2,7 +2,7 @@ import SwiftUI struct MainHomeBusinessInfoSection: View { var body: some View { - BusinessInfoExpandableTextView( + V2ExpandableTextView( text: I18n.Settings.companyInfo, lineLimit: 3, moreTitle: I18n.HomeRecommendation.more, @@ -15,57 +15,6 @@ struct MainHomeBusinessInfoSection: View { } } -private struct BusinessInfoExpandableTextView: View { - let text: String - let lineLimit: Int - let moreTitle: String - let collapseTitle: String - let font: SodaTypography - let foregroundColor: Color - - @State private var isExpanded = false - @State private var fullTextHeight: CGFloat = 0 - @State private var limitedTextHeight: CGFloat = 0 - - var body: some View { - VStack(alignment: .leading, spacing: SodaSpacing.s8) { - Text(text) - .appFont(font) - .foregroundColor(foregroundColor) - .lineLimit(isExpanded ? nil : lineLimit) - .background(measuringText(lineLimit: nil, height: $fullTextHeight)) - .background(measuringText(lineLimit: lineLimit, height: $limitedTextHeight)) - - if fullTextHeight > limitedTextHeight + 1 { - Button { - isExpanded.toggle() - } label: { - Text(isExpanded ? collapseTitle : moreTitle) - .appFont(.body5) - .foregroundColor(.white) - } - .buttonStyle(.plain) - } - } - } - - private func measuringText(lineLimit: Int?, height: Binding) -> some View { - Text(text) - .appFont(font) - .lineLimit(lineLimit) - .fixedSize(horizontal: false, vertical: true) - .hidden() - .background( - GeometryReader { proxy in - Color.clear - .onAppear { height.wrappedValue = proxy.size.height } - .onChange(of: proxy.size.height) { newHeight in height.wrappedValue = newHeight } - .onChange(of: text) { _ in height.wrappedValue = proxy.size.height } - } - ) - } -} - struct MainHomeBusinessInfoSection_Previews: PreviewProvider { static var previews: some View { MainHomeBusinessInfoSection() diff --git a/SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift b/SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift index e8a77a3d..49aef158 100644 --- a/SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift +++ b/SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift @@ -48,6 +48,7 @@ final class MainHomeRecommendationViewModel: ObservableObject { self.isShowPopup = true } } catch { + ERROR_LOG(error.localizedDescription) self.errorMessage = I18n.Common.commonError self.isShowPopup = true } diff --git a/SodaLive/Sources/V2/Main/Home/Recommendation/Models/MainHomeRecommendationResponse.swift b/SodaLive/Sources/V2/Main/Home/Recommendation/Models/MainHomeRecommendationResponse.swift index 9d8b6316..2725c66a 100644 --- a/SodaLive/Sources/V2/Main/Home/Recommendation/Models/MainHomeRecommendationResponse.swift +++ b/SodaLive/Sources/V2/Main/Home/Recommendation/Models/MainHomeRecommendationResponse.swift @@ -5,7 +5,6 @@ struct HomeRecommendationResponse: Decodable { let banners: [RecommendationBannerResponse] let recentlyActiveCreators: [HomeActiveCreatorItem] let recentDebutCreators: [HomeCreatorItem] - let firstAudioContents: [HomeFirstAudioContentItem] let aiCharacters: [HomeAiCharacterItem] let genreCreators: [HomeGenreCreatorGroupItem] let cheerCreators: [HomeCreatorItem] @@ -67,17 +66,6 @@ struct HomeCreatorItem: Decodable, Hashable { let creatorProfileImage: String } -struct HomeFirstAudioContentItem: Decodable, Hashable { - let contentId: Int - let creatorId: Int - let creatorNickname: String - let creatorProfileImage: String - let title: String - let price: Int - let coverImage: String? - let isPointAvailable: Bool -} - struct HomeAiCharacterItem: Decodable, Hashable { let characterId: Int let creatorId: Int diff --git a/SodaLive/Sources/V2/Main/MainView.swift b/SodaLive/Sources/V2/Main/MainView.swift index a64b76dc..b31aeb5d 100644 --- a/SodaLive/Sources/V2/Main/MainView.swift +++ b/SodaLive/Sources/V2/Main/MainView.swift @@ -24,8 +24,10 @@ struct MainView: View { @State private var isShowPlayer = false @State private var isShowAuthView = false @State private var isShowAuthConfirmView = false + @State private var isShowRecommendationFollowAllConfirmDialog = false @State private var isCreatorActionMenuPresented = false @State private var pendingAction: (() -> Void)? = nil + @State private var pendingRecommendationFollowAllAction: (() -> Void)? = nil @State private var payload = Payload() @State private var hasLoadedMainData = false @@ -75,6 +77,10 @@ struct MainView: View { authConfirmDialog } + if isShowRecommendationFollowAllConfirmDialog { + recommendationFollowAllConfirmDialog + } + if liveViewModel.isShowPaymentDialog { LivePaymentDialog( title: liveViewModel.paymentDialogTitle, @@ -177,7 +183,7 @@ struct MainView: View { onTapLive: handleRecommendationLiveTap, onTapCreator: handleRecommendationCreatorTap, onTapContent: handleRecommendationContentTap, - onTapCharacter: handleRecommendationCreatorTap, + onTapCharacter: handleRecommendationCharacterTap, onTapCommunityPost: handleRecommendationCommunityPostTap, onTapBanner: handleRecommendationBannerTap, onTapLiveAll: handleRecommendationLiveAllTap, @@ -323,6 +329,34 @@ struct MainView: View { pendingAction = nil } + private var recommendationFollowAllConfirmDialog: some View { + SodaV2ActionModal( + title: I18n.HomeRecommendation.followAllConfirmTitle, + message: I18n.HomeRecommendation.followAllConfirmDescription, + button1: SodaV2ActionModalButton( + label: I18n.HomeRecommendation.followAllConfirmAction, + action: confirmRecommendationFollowAll + ), + button2: SodaV2ActionModalButton( + label: I18n.Common.cancel, + action: dismissRecommendationFollowAllConfirmDialog + ), + onDimmedTap: dismissRecommendationFollowAllConfirmDialog + ) + } + + private func confirmRecommendationFollowAll() { + let action = pendingRecommendationFollowAllAction + isShowRecommendationFollowAllConfirmDialog = false + pendingRecommendationFollowAllAction = nil + action?() + } + + private func dismissRecommendationFollowAllConfirmDialog() { + isShowRecommendationFollowAllConfirmDialog = false + pendingRecommendationFollowAllAction = nil + } + private var authView: some View { BootpayUI(payload: payload, requestType: BootpayRequest.TYPE_AUTHENTICATION) .onConfirm { _ in true } @@ -502,6 +536,13 @@ struct MainView: View { } } + private func handleRecommendationCharacterTap(characterId: Int) { + guard characterId > 0 else { return } + performRecommendationDetailAction(requiresAdultContent: true) { + appState.setAppStep(step: .characterDetail(characterId: characterId)) + } + } + private func handleRecommendationCommunityPostTap(postId: Int) { guard postId > 0 else { return } performRecommendationDetailAction { @@ -510,7 +551,14 @@ struct MainView: View { } private func handleRecommendationFollowAllTap(action: @escaping () -> Void) { - performRecommendationDetailAction(action: action) + let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { + AppState.shared.setAppStep(step: .login) + return + } + + pendingRecommendationFollowAllAction = action + isShowRecommendationFollowAllConfirmDialog = true } private func handleFollowingAllTap() { @@ -681,7 +729,9 @@ struct MainView: View { let isKoreanCountry = normalizedCountryCode.isEmpty || normalizedCountryCode == "KR" if isKoreanCountry && auth == false { - pendingAction = action + pendingAction = { + performRecommendationDetailAction(requiresAdultContent: true, action: action) + } isShowAuthConfirmView = true return } diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md b/docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md index 40d17320..3891f105 100644 --- a/docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md +++ b/docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md @@ -42,7 +42,7 @@ - `SodaLive/Sources/V2/Main/Home/Recommendation/Repository/MainHomeRecommendationRepository.swift` - `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift` - `SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift` -- `SodaLive/Sources/V2/Component/Text/ExpandableTextView.swift` +- `SodaLive/Sources/V2/Component/Text/V2ExpandableTextView.swift` - `SodaLive/Sources/I18n/I18n.swift` - `SodaLive.xcodeproj/project.pbxproj` @@ -125,6 +125,23 @@ - 실행 명령: `rg "roomId|creatorProfileImage|targetId|totalChatCount|originalWorkTitle|audioUrl" SodaLive/Sources/V2/Main/Home/Recommendation/Models/MainHomeRecommendationResponse.swift` - 기대 결과: PRD 핵심 필드가 모델에 반영되어야 한다. +- [x] **Task 2.3: PRD 응답 계약에 없는 필수 디코딩 필드 제거** + - Goal 실행 `P2-R1`: `REV-P2-001`을 수정해 PRD 기준 추천 응답이 `firstAudioContents` 없이도 정상 디코딩되게 한다. + - 시작 조건: + - 리뷰: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase2-recommendation-api-response-model.md` + - 확정 항목: `REV-P2-001` + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Main/Home/Recommendation/Models/MainHomeRecommendationResponse.swift` + - TDD 예외 사유: + - 앱 테스트 번들 타깃이 없어 production 모델을 직접 import하는 영구 unit test를 이번 Task에서 추가할 수 없다. + - 대체 검증 방법: + - PRD의 필수 필드만 포함하고 `firstAudioContents`가 없는 JSON으로 디코딩 회귀를 확인하고, 정적 검색과 앱 빌드를 실행한다. + - 작업 내용: + - [x] 현재 모델이 기준 JSON에서 `keyNotFound(firstAudioContents)`로 실패하는 것을 재현한다. + - [x] 실제 사용 경로와 PRD 계약이 없는 `firstAudioContents` 및 `HomeFirstAudioContentItem`을 제거한다. + - [x] 기준 JSON 디코딩, `rg "firstAudioContents|HomeFirstAudioContentItem" SodaLive/Sources/V2/Main/Home/Recommendation`, `git diff --check`, `SodaLive-dev` Debug 빌드를 통과한다. + - [x] 수정 결과와 실제 명령을 검증 기록에 누적한다. + ### Phase 3: 활동 타입과 I18n - [x] **Task 3.1: RecommendedActivityType 변환 보완** @@ -157,6 +174,23 @@ - 실행 명령: `rg "final class MainHomeRecommendationViewModel|fetchRecommendations|HomeRecommendationResponse|MainHomeRecommendationRepository|isLoading|errorMessage" SodaLive/Sources/V2/Main/Home` - 기대 결과: 추천 API 로딩/에러/응답 상태가 `MainHomeRecommendationViewModel` 중심으로 정리되어야 한다. +- [x] **Task 4.2: 추천 응답 디코딩 오류 로그를 프로젝트 로깅 규칙에 맞춤** + - Goal 실행 `P4-R1`: `REV-P4-001`을 수정해 신규 추천 ViewModel의 디코딩 오류가 `print`가 아니라 프로젝트 오류 로그 경로로 기록되게 한다. + - 시작 조건: + - 리뷰: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase4-recommendation-state-layer.md` + - 확정 항목: `REV-P4-001` + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift` + - TDD 예외 사유: + - 사용자 동작을 바꾸지 않는 오류 로그 호출 교체이며 앱 테스트 번들 타깃이 없다. + - 대체 검증 방법: + - 현재 `print(error)` 사용을 정적 재현하고 프로젝트 `ERROR_LOG` 관례와 대조한 뒤 정적 검색과 앱 빌드로 확인한다. + - 작업 내용: + - [x] 추천 응답 디코딩 `catch`의 `print(error)`를 실패 근거로 확인한다. + - [x] 기존 toast·loading 처리는 유지하고 오류 기록만 `ERROR_LOG(error.localizedDescription)`로 교체한다. + - [x] 추천 범위의 `print(` 미사용, `ERROR_LOG` 사용, `git diff --check`와 `SodaLive-dev` Debug 빌드를 확인한다. + - [x] 수정 결과와 실제 명령을 이 문서와 Phase 4 리뷰 리포트에 누적한다. + ### Phase 5: 추천 상세 진입 데이터/라우팅 guard - [x] **Task 5.1: 추천 탭 상세 진입 guard 적용** @@ -180,6 +214,63 @@ - 기대 결과: 신규 추천 탭 상세 진입 callback이 기존 인증/민감 콘텐츠 guard 인프라를 재사용한다. - 수동 확인: `MainHomeView`, `MainHomeRecommendationView` 내부에 Bootpay 인증 UI를 중복 구현하지 않아야 한다. +- [x] **Task 5.2: AI 캐릭터 상세 라우팅과 guard 연결** + - Goal 실행 `P5-R1`: `REV-P5-001`을 수정해 추천 AI 캐릭터 callback이 로그인·민감 콘텐츠 guard를 거쳐 `characterDetail(characterId:)`로 이동하게 한다. + - 시작 조건: + - 리뷰: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase5-detail-routing-guards.md` + - 확정 항목: `REV-P5-001` + - `Task 12.2`의 `characterId` 전달 보정과 함께 검증한다. + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Main/MainView.swift` + - 확인: `SodaLive/Sources/Home/HomeTabView.swift` + - TDD 예외 사유: + - 앱 테스트 번들 타깃이 없고 SwiftUI callback에서 전역 `AppState` route까지 확인하는 테스트 인프라 추가는 이번 회귀 수정 범위를 벗어난다. + - 대체 검증 방법: + - callback 연결 정적 검색, `SodaLive-dev` 빌드, `creatorId != characterId`인 항목의 수동 탭으로 검증한다. + - 작업 내용: + - [x] `onTapCharacter`가 `handleRecommendationCreatorTap`에 연결되어 크리에이터 상세로 이동하는 현재 실패 경로를 확인한다. + - [x] 양수 `characterId`를 검증하고 기존 추천 상세 guard를 재사용해 `.characterDetail(characterId:)`를 실행하는 전용 handler를 연결한다. + - [x] `rg "onTapCharacter:|handleRecommendationCharacterTap|characterDetail\\(characterId:" SodaLive/Sources/V2/Main/MainView.swift`, `git diff --check`, `SodaLive-dev` Debug 빌드를 통과한다. + - [x] 비로그인·본인인증·민감 콘텐츠 설정·정상 진입 흐름은 기존 guard 재사용 정적 검색과 빌드로 확인하고, 실제 기기 수동 확인은 잔여 확인 사항으로 검증 기록에 남긴다. + +- [x] **Task 5.3: 본인인증 후 민감 콘텐츠 설정 guard 재검사** + - Goal 실행 `P5-R2`: `REV-P5-002`를 수정해 한국 사용자의 본인인증 성공 후에도 민감 콘텐츠 보기 설정을 확인한 뒤 AI 캐릭터 상세로 이동하게 한다. + - 시작 조건: + - 리뷰: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase5-detail-routing-guards.md` + - 확정 항목: `REV-P5-002` + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Main/MainView.swift` + - TDD 예외 사유: + - 앱 테스트 번들 타깃이 없고 Bootpay 본인인증 callback과 전역 `AppState` route를 직접 실행하는 영구 통합 test 인프라는 이번 회귀 수정 범위를 벗어난다. + - 대체 검증 방법: + - `token != empty`, 한국 사용자, `auth == false`, `isAdultContentVisible == false` 상태 전이를 최소 Swift self-check로 재현하고, 정적 callback 추적·앱 빌드·수동 상태 조합 확인으로 검증한다. + - 작업 내용: + - [x] **RED:** 본인인증 전 저장한 `pendingAction`이 인증 성공 callback에서 상세 route를 직접 실행해 민감 콘텐츠 설정 화면을 건너뛰는 현재 실패를 재현한다. + - [x] **GREEN:** 인증 성공 후 보류 action이 상세 route를 직접 실행하지 않고 기존 `performRecommendationDetailAction(requiresAdultContent: true)`의 남은 설정 guard를 다시 통과하도록 최소 수정한다. + - [x] **REFACTOR:** 기존 로그인, 비한국 사용자, 이미 본인인증된 사용자, 민감 콘텐츠 설정 활성 사용자 흐름은 변경하지 않고 중복 guard를 추가하지 않는다. + - [x] 한국 사용자 기준 `auth=false/설정=false`, `auth=false/설정=true`, `auth=true/설정=false`, `auth=true/설정=true` 조합과 취소·오류 시 보류 action 미실행을 확인한다. + - [x] `git diff --check`, `plutil -lint SodaLive.xcodeproj/project.pbxproj`, `SodaLive-dev` Debug 빌드를 실행하고 결과를 이 문서와 Phase 5 리뷰 리포트에 누적한다. + +- [x] **Task 5.4: 활성 민감 콘텐츠 진입 경로의 인증 후 설정 guard 재검사** + - Goal 실행 `P5-R3`: `REV-P5-003`을 수정해 라이브 전체 화면의 성인 라이브와 크리에이터 채널 AI 대화도 본인인증 성공 후 민감 콘텐츠 보기 설정을 다시 확인하게 한다. + - 시작 조건: + - 리뷰: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase5-detail-routing-guards.md` + - 확정 항목: `REV-P5-003` + - 대상 파일: + - 수정: `SodaLive/Sources/Live/Now/All/LiveNowAllView.swift` + - 수정: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift` + - 확인: `SodaLive/Sources/ContentView.swift` + - TDD 예외 사유: + - 앱 테스트 번들 타깃이 없고 Bootpay 본인인증 callback과 전역 route를 직접 실행하는 영구 통합 test 인프라는 이번 회귀 수정 범위를 벗어난다. + - 대체 검증 방법: + - 두 활성 화면의 `auth=false`, 민감 콘텐츠 설정 비활성 상태 전이를 최소 Swift self-check로 재현하고, callback 추적·앱 빌드·수동 상태 조합으로 확인한다. + - 작업 내용: + - [x] **RED:** `LiveNowAllView.handleLiveNowItemTap`과 `CreatorChannelView.handleTalkTap`이 인증 성공 후 설정 화면 없이 보호 route를 직접 실행하는 상태를 재현한다. + - [x] **GREEN:** 각 화면의 `pendingAction`이 최종 route가 아니라 기존 진입 함수를 다시 호출해 남은 설정 guard를 통과하도록 최소 수정한다. + - [x] **REFACTOR:** 공용 계층을 새로 만들지 않고 기존 로그인·국가·본인인증·설정 분기와 route를 유지한다. + - [x] 두 화면에서 `auth=false/설정=false`, `auth=false/설정=true`, `auth=true/설정=false`, `auth=true/설정=true` 조합과 인증 취소·오류를 확인한다. + - [x] `git diff --check`, `plutil -lint SodaLive.xcodeproj/project.pbxproj`, `SodaLive-dev`와 `SodaLive` Debug 빌드를 실행하고 결과를 이 문서와 Phase 5 리뷰 리포트에 누적한다. + ### Phase 6: 홈 탭 shell과 탭별 루트 연결 - [x] **Task 6.1: 추천/랭킹/팔로잉 하위 폴더와 루트 View 준비** @@ -217,6 +308,26 @@ - 실행 명령: `rg "case \\.home|MainHomeView|MainPlaceholderTabView" SodaLive/Sources/V2/Main/MainView.swift` - 기대 결과: `.home`은 `MainHomeView()`를 표시하고, 다른 탭은 기존 동작을 유지한다. +- [x] **Task 6.4: 홈 상단 탭 제목 현지화 연결** + - Goal 실행 `P6-R1`: `REV-P6-001`을 수정해 추천/랭킹/팔로잉 탭 제목이 앱 언어 ko/en/ja에 맞게 표시되도록 한다. + - 시작 조건: + - 리뷰: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase6-home-tab-shell.md` + - 확정 항목: `REV-P6-001` + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Main/Home/MainHomeView.swift` + - 확인: `SodaLive/Sources/I18n/I18n.swift` + - TDD 예외 사유: + - 앱 테스트 번들 타깃이 없어 SwiftUI 탭 제목의 언어별 렌더링을 직접 검증하는 영구 UI test를 이번 Task에서 추가할 수 없다. + - 대체 검증 방법: + - 기존 `I18n.MainContentRecommendation.tabTitle`, `I18n.MainContentRanking.tabTitle`, `I18n.HomeFollowing.tabTitle` 재사용 여부를 정적 검색하고 ko/en/ja 화면과 앱 빌드로 확인한다. + - 작업 내용: + - `MainHomeView`의 탭 제목에 남은 한글 문자열 리터럴을 기존 I18n 키로 교체한다. + - 새 문자열이나 별도 매핑 계층을 추가하지 않는다. + - 검증 기준: + - 실행 명령: `rg '"추천"|"랭킹"|"팔로잉"|MainContentRecommendation\.tabTitle|MainContentRanking\.tabTitle|HomeFollowing\.tabTitle' SodaLive/Sources/V2/Main/Home/MainHomeView.swift SodaLive/Sources/I18n/I18n.swift` + - 기대 결과: `MainHomeView`는 기존 I18n 탭 제목만 사용하고 한글 탭 제목 리터럴은 없어야 한다. + - 수동 확인: 앱 언어 ko/en/ja에서 세 탭 제목이 각 언어로 표시되어야 한다. + ### Phase 7: 데이터 기반 프로젝트 포함과 중간 빌드 검증 - [x] **Task 7.1: 데이터 기반 신규 Swift 파일 프로젝트 포함 여부 확인** @@ -315,6 +426,26 @@ - 기대 결과: `BUILD SUCCEEDED` - 수동 확인: 추천 홈에서 배너가 Figma 노드 `24:5525`처럼 좌우 배너 일부 노출, 간격 8, 1:1 이미지, 우측 상단 인디케이터 형태로 표시되어야 한다. +- [x] **Task 9.5: 무한 캐러셀 경계 연속 스와이프를 안전하게 처리** + - Goal 실행 `P9-R1`: `REV-P9-001`을 수정해 첫·마지막 복제 배너 전환 중 연속 스와이프가 발생해도 표시 index가 유효 범위를 벗어나지 않게 한다. + - 시작 조건: + - 리뷰: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase9-banner-section.md` + - 확정 항목: `REV-P9-001` + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Component/Banner/BannerCarousel.swift` + - TDD 예외 사유: + - 앱 테스트 번들 타깃이 없어 SwiftUI gesture와 지연 animation을 직접 실행하는 영구 UI test를 이번 Task에서 추가할 수 없다. + - 대체 검증 방법: + - 마지막 실데이터 index에서 지연 reset 전에 같은 방향과 반대 방향 스와이프를 각각 적용하는 상태 전이, 첫 index의 대칭 상태 전이와 데이터 변경 중 예약된 reset을 재현하고, 수동 경계 연속 스와이프와 앱 빌드로 확인한다. + - 작업 내용: + - [x] `displayIndex == items.count`에서 두 번 연속 증가하면 `items.count + 2`가 되고 현재 `wrapIfNeeded()`가 이를 복구하지 못하는 실패를 재현한다. + - [x] 전환 지연 중 추가 gesture·timer 전이가 index를 유효 복제 범위 밖으로 보내지 않도록 최소 상태/guard만 추가한다. + - [x] 배너 크기, 간격, 페이지 형식, 5초 자동 스크롤과 탭 callback은 변경하지 않는다. + - [x] 경계에서 예약한 지연 reset이 사용자의 반대 방향 이동 또는 `items` 변경 뒤 현재 정상 index를 덮어쓰지 않도록, reset 실행 시점에 여전히 같은 복제 경계인지 확인한다. + - [x] 마지막·첫 경계에서 같은 방향/반대 방향 입력과 예약 reset 실행 순서를 포함한 상태 전이 self-check를 통과한다. + - [x] 첫·마지막 경계의 빠른 양방향 연속 스와이프, 자동 스크롤, 페이지 표시를 수동 확인하고 `git diff --check`와 `SodaLive-dev` Debug 빌드를 실행한다. + - [x] 수정 결과와 실제 명령을 이 문서와 Phase 9 리뷰 리포트에 누적한다. + ### Phase 10: 최근 활동 크리에이터 섹션 UI - [x] **Task 10.1: 최근 활동 크리에이터 섹션 작성/보완** @@ -328,6 +459,24 @@ - 실행 명령: `rg "MainHomeActiveCreatorSection|activityType|activityAt|targetId|RecommendedActivityType" SodaLive/Sources/V2/Main/Home/Recommendation` - 기대 결과: 최근 활동 섹션이 신규 응답 필드와 활동 타입 변환을 사용한다. +- [x] **Task 10.2: 최근 활동 상대 시간 계산을 공용 DateParser로 단일화** + - Goal 실행 `P10-R1`: `REV-P10-001`을 수정해 최근 활동 섹션의 중복 상대 시간 계산을 제거하고 공용 `DateParser.relativeTimeText`를 재사용한다. + - 시작 조건: + - 리뷰: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase10-active-creators.md` + - 확정 항목: `REV-P10-001` + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeActiveCreatorSection.swift` + - 확인: `SodaLive/Sources/Common/DateParser.swift` + - TDD 예외 사유: + - 사용자 동작을 바꾸지 않고 공용 함수와 동일한 중복 계산만 제거하는 구조 정리 Task이며 앱 테스트 번들 타깃이 없다. + - 대체 검증 방법: + - 두 구현의 분기와 반환 I18n을 대조하고, 공용 함수 연결 정적 검색 및 앱 빌드로 동작 보존을 확인한다. + - 작업 내용: + - [x] `relativeActivityAtText(now:)`와 `DateParser.relativeTimeText(fromUTC:fallback:now:)`가 동일한 연/월/일/시/분 분기와 fallback을 구현하는 현재 중복을 확인한다. + - [x] 최근 활동 표시를 공용 `DateParser.relativeTimeText` 호출로 교체하고 이번 섹션의 중복 계산만 제거한다. + - [x] `rg "dateComponents|timeIntervalSince|relativeTimeText" SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeActiveCreatorSection.swift SodaLive/Sources/Common/DateParser.swift`, `git diff --check`, `SodaLive-dev` Debug 빌드를 실행한다. + - [x] 수정 결과와 실제 명령을 이 문서와 Phase 10 리뷰 리포트에 누적한다. + ### Phase 11: 최근 데뷔한 크리에이터 섹션 UI - [x] **Task 11.1: 최근 데뷔 크리에이터 섹션 작성/보완** @@ -337,11 +486,11 @@ - 확인/보완: `SodaLive/Sources/V2/Component/Creator/CreatorProfileItem.swift` - 작업 내용: - `HomeCreatorItem.creatorId`, `creatorNickname`, `creatorProfileImage`를 사용한다. - - 기존 `CreatorProfileGrid`와 `CreatorProfileItem`을 우선 재사용한다. + - 기존 `CreatorProfileGrid`와 `CreatorProfileItem`을 우선 검토하되, Figma 직사각형 카드 형태를 충족하지 못하면 페이지 전용 `MainHomeRecentDebutCreatorItemView`를 사용한다. - 데이터가 비어 있으면 섹션을 표시하지 않는다. - 검증 기준: - - 실행 명령: `rg "MainHomeRecentDebutCreatorSection|CreatorProfileGrid|CreatorProfileItem|creatorProfileImage" SodaLive/Sources/V2/Main/Home/Recommendation SodaLive/Sources/V2/Component/Creator` - - 기대 결과: 최근 데뷔 섹션이 공용 Creator widget을 재사용한다. + - 실행 명령: `rg "MainHomeRecentDebutCreatorSection|MainHomeRecentDebutCreatorItemView|CreatorProfileGrid|CreatorProfileItem|creatorProfileImage" SodaLive/Sources/V2/Main/Home/Recommendation SodaLive/Sources/V2/Component/Creator` + - 기대 결과: 최근 데뷔 섹션이 신규 응답 필드를 사용하고, 공용 원형 widget으로 충족할 수 없는 Figma 직사각형 카드는 페이지 전용 View를 사용한다. ### Phase 12: AI 캐릭터 섹션 UI @@ -357,6 +506,24 @@ - 실행 명령: `rg "MainHomeAiCharacterSection|AiCharacterCard|totalChatCount|originalWorkTitle|profileImage" SodaLive/Sources/V2` - 기대 결과: AI 캐릭터 섹션이 신규 응답 필드를 기준으로 구성된다. +- [x] **Task 12.2: AI 캐릭터 탭 식별자를 characterId로 보정** + - Goal 실행 `P12-R1`: `REV-P12-001`을 수정해 AI 캐릭터 카드가 `creatorId`가 아니라 `characterId`를 상세 callback으로 전달하게 한다. + - 시작 조건: + - 리뷰: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase12-ai-character-section.md` + - 확정 항목: `REV-P12-001` + - `Task 5.2`의 최종 character route가 준비되어야 한다. + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeAiCharacterSection.swift` + - TDD 예외 사유: + - 앱 테스트 번들 타깃이 없어 SwiftUI Button callback을 직접 실행하는 영구 unit test를 추가할 수 없다. + - 대체 검증 방법: + - Preview처럼 `characterId != creatorId`인 fixture의 callback 값을 수동 확인하고 정적 검색과 앱 빌드를 실행한다. + - 작업 내용: + - [x] 현재 `onTapCharacter(item.creatorId)` 전달을 실패 근거로 확인한다. + - [x] 최소 수정으로 `onTapCharacter(item.characterId)`를 전달한다. + - [x] `rg "onTapCharacter\\(item\\.(characterId|creatorId)\\)" SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeAiCharacterSection.swift`, `git diff --check`, `SodaLive-dev` Debug 빌드를 통과한다. + - [x] `creatorId != characterId`인 항목은 `characterId` callback과 `characterDetail(characterId:)` 정적 연결로 확인하고, 실제 탭 수동 확인은 잔여 확인 사항으로 검증 기록에 남긴다. + ### Phase 13: 장르의 크리에이터 섹션 UI - [x] **Task 13.1: 장르 크리에이터 그룹 섹션 작성/보완** @@ -410,6 +577,45 @@ - 실행 명령: `rg "MainHomePopularCommunitySection|CommunityPostCard|imageUrl|audioUrl|existOrdered|구매완료|purchaseCommunityPost|communityPostLike|toggleContent|relativeTimeText" SodaLive/Sources/V2 SodaLive/Sources/Common SodaLive/Sources/Explorer/Profile/CreatorCommunity` - 기대 결과: 커뮤니티 섹션과 카드 조건 분기, 오디오 재생, 구매/좋아요 연결, UTC 상대 날짜 변환이 확인되고 `구매완료` 표시 구현은 없어야 한다. +- [x] **Task 15.2: 무료 커뮤니티 게시물 가격 경계 보정** + - Goal 실행 `P15-R1`: `REV-P15-001`을 수정해 PRD의 `price <= 0 || existOrdered` 조건과 카드 접근 가능 상태를 일치시킨다. + - 시작 조건: + - 리뷰: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase15-popular-community-feed.md` + - 확정 항목: `REV-P15-001` + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift` + - TDD 예외 사유: + - 앱 테스트 번들 타깃이 없어 private SwiftUI 계산 프로퍼티의 영구 unit test를 추가할 수 없다. + - 대체 검증 방법: + - `price`가 음수·0·양수인 상태와 `existOrdered` 조합을 Preview/수동 확인하고 정적 검색과 앱 빌드를 실행한다. + - 작업 내용: + - [x] `price < 0 && existOrdered == false`에서 현재 `isAccessiblePost == false`가 되는 경계를 확인한다. + - [x] `isAccessiblePost`를 PRD의 `price <= 0 || existOrdered` 조건과 일치시키는 최소 수정만 적용한다. + - [x] `rg "price (==|<=) 0 \\|\\| existOrdered" SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift`, `git diff --check`, `SodaLive-dev` Debug 빌드를 통과한다. + - [x] 무료·유료 미구매·구매 완료 상태 조건은 정적 검색과 빌드로 확인하고, 실제 화면 수동 확인은 잔여 확인 사항으로 검증 기록에 남긴다. + +- [x] **Task 15.3: 이미지 없는 유료 미구매 게시물 구매 진입 보완** + - Goal 실행 `P15-R2`: `REV-P15-002`를 수정해 `imageUrl == nil && price > 0 && existOrdered == false`인 게시물에서도 기존 구매 API로 진입할 수 있게 한다. + - 시작 조건: + - 리뷰: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase15-popular-community-feed.md` + - 확정 항목: `REV-P15-002` + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift` + - 확인/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomePopularCommunitySection.swift` + - TDD 예외 사유: + - 앱 테스트 번들 타깃이 없어 private SwiftUI 상태와 구매 Button action을 직접 실행하는 영구 unit/UI test를 이번 Task에서 추가할 수 없다. + - 대체 검증 방법: + - 이미지 유무·가격·구매 여부 조합 fixture를 Preview 또는 수동 화면으로 확인하고 callback 정적 검색과 앱 빌드를 실행한다. + - 작업 내용: + - 이미지 없는 레이아웃은 유지하면서 유료 미구매 상태에 구매 affordance를 제공한다. + - 기존 `purchaseCommunityPost` callback과 loading·오류 흐름을 재사용한다. + - 무료 및 구매 완료 게시물의 카드 탭·반응 UI는 변경하지 않는다. + - 검증 기준: + - 수동 확인: `imageUrl == nil`, `price > 0`, `existOrdered == false` fixture에서 구매 진입이 노출되고 한 번의 사용자 확인으로 기존 구매 callback이 1회 실행되어야 한다. + - 수동 확인: 이미지가 있는 유료 미구매, 무료, 구매 완료 상태는 기존 표시와 동작을 유지해야 한다. + - 실행 명령: `rg "hasImage|isLocked|paidLockOverlay|purchaseCommunityPost|onTapPurchase" SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift SodaLive/Sources/V2/Main/Home/Recommendation` + - 기대 결과: 이미지 유무와 무관하게 유료 미구매 상태가 기존 구매 경로에 연결되어야 한다. + ### Phase 16: 사업자 정보 섹션 UI - [x] **Task 16.1: 사업자 정보 더보기/접기 섹션 작성/보완** @@ -426,18 +632,61 @@ - 실행 명령: `rg "MainHomeBusinessInfoSection|BusinessInfoExpandableTextView|ExpandableTextView|lineLimit|더보기|접기" SodaLive/Sources/V2` - 기대 결과: 사업자 정보 섹션과 확장 텍스트 구현이 확인된다. +- [x] **Task 16.2: 사업자 정보 확장 텍스트 구현 단일화** + - Goal 실행 `P16-R1`: `REV-P16-001`을 수정해 사업자 정보 wrapper가 빌드 대상의 공용 V2 확장 텍스트 하나만 사용하게 한다. + - 시작 조건: + - 리뷰: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase16-business-info-section.md` + - 확정 항목: `REV-P16-001` + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeBusinessInfoSection.swift` + - 수정/이름 정리: `SodaLive/Sources/V2/Component/Text/ExpandableTextView.swift` + - 확인/보완: `SodaLive.xcodeproj/project.pbxproj` + - 확인: `SodaLive/Sources/CustomView/ExpandableTextView.swift` + - TDD 예외 사유: + - 동작을 추가하지 않고 중복 View와 Xcode target 포함 상태를 정리하는 회귀 문서/구조 Task다. + - 대체 검증 방법: + - 타입 정의·소비처·PBX Sources 포함 개수를 대조하고 3줄 이하/초과 텍스트 수동 확인과 앱 빌드를 실행한다. + - 작업 내용: + - [x] 현재 V2 공용 파일이 Sources에 포함되지 않고 화면 파일에 같은 측정 구현이 복제된 상태를 확인한다. + - [x] 기존 `CustomView/ExpandableTextView` 소비처를 변경하지 않으면서 타입명 충돌을 피하고, 공용 V2 구현과 사업자 wrapper를 단일 연결한다. + - [x] 이번 변경으로 생긴 미사용 private 구현과 프로젝트 참조만 정리한다. + - [x] 타입 정의/소비처 검색, `plutil -lint SodaLive.xcodeproj/project.pbxproj`, `git diff --check`, `SodaLive-dev` Debug 빌드를 통과한다. + - [x] 3줄 이하/초과 더보기 조건은 공용 구현 연결, 타입 검색, 빌드로 확인하고, 실제 화면 수동 확인은 잔여 확인 사항으로 검증 기록에 남긴다. + +- [x] **Task 16.3: V2 확장 텍스트 Sources 중복 등록 제거** + - Goal 실행 `P16-R2`: `REV-P16-002`를 수정해 `V2ExpandableTextView.swift`가 각 앱 target의 Sources에 정확히 한 번만 포함되게 한다. + - 시작 조건: + - 리뷰: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase16-business-info-section.md` + - 확정 항목: `REV-P16-002` + - 대상 파일: + - 수정: `SodaLive.xcodeproj/project.pbxproj` + - TDD 예외 사유: + - production 동작이 아니라 Xcode target의 Sources 메타데이터 중복을 정리하는 프로젝트 설정 Task다. + - 대체 검증 방법: + - PBX Sources phase별 포함 개수를 대조하고 프로젝트 유효성 검사와 두 앱 scheme 빌드를 실행한다. + - 작업 내용: + - `SodaLive` target에 중복 등록된 `V2ExpandableTextView.swift in Sources` 한 건만 제거한다. + - `SodaLive-dev` target 등록과 다른 기존 Sources 항목은 변경하지 않는다. + - 검증 기준: + - 실행 명령: 앱 target별 Sources build phase에서 `V2ExpandableTextView.swift in Sources` 포함 개수를 확인한다. + - 기대 결과: `SodaLive`, `SodaLive-dev` target에 각각 정확히 한 번 포함되어야 한다. + - 실행 명령: `plutil -lint SodaLive.xcodeproj/project.pbxproj` + - 기대 결과: `OK` + - 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build` 및 `SodaLive-dev` 동일 빌드 + - 기대 결과: 두 scheme 모두 `BUILD SUCCEEDED`이며 해당 파일의 중복 Sources 경고가 없어야 한다. + ### Phase 17: 섹션 조립 순서와 빈 섹션 처리 - [x] **Task 17.1: MainHomeRecommendationView에 섹션 순서 반영** - 대상 파일: - 확인/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift` - 작업 내용: - - 섹션 순서를 PRD와 맞춘다. - - 현재 라이브, 배너, 최근 활동, 최근 데뷔, AI 캐릭터, 장르, 응원, 인기 커뮤니티, 사업자 정보 순서로 배치한다. + - 섹션 순서를 PRD와 2026-07-12 장르 크리에이터 섹션 미표시 결정에 맞춘다. + - 현재 라이브, 배너, 최근 활동, 최근 데뷔, AI 캐릭터, 응원, 인기 커뮤니티, 사업자 정보 순서로 배치하고 장르 섹션은 조립하지 않는다. - 각 데이터 배열이 비어 있으면 섹션 제목과 컨테이너까지 숨긴다. - 검증 기준: - - 실행 명령: `rg "MainHomeLiveSection|MainHomeBannerSection|MainHomeActiveCreatorSection|MainHomeRecentDebutCreatorSection|MainHomeAiCharacterSection|MainHomeGenreCreatorSection|MainHomeCheerCreatorSection|MainHomePopularCommunitySection|MainHomeBusinessInfoSection" SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift` - - 기대 결과: 모든 구현 대상 섹션이 올바른 순서로 조립된다. + - 실행 명령: `rg "MainHomeLiveSection|MainHomeBannerSection|MainHomeActiveCreatorSection|MainHomeRecentDebutCreatorSection|MainHomeAiCharacterSection|MainHomeCheerCreatorSection|MainHomePopularCommunitySection|MainHomeBusinessInfoSection" SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift` + - 기대 결과: 활성 구현 대상 섹션이 올바른 순서로 조립되고 `MainHomeGenreCreatorSection`은 검색되지 않는다. ### Phase 18: 최종 프로젝트 포함과 빌드 검증 @@ -474,6 +723,42 @@ - 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` - 기대 결과: `BUILD SUCCEEDED` +- [x] **Task 18.3: 프로덕션 target의 홈 추천 Sources 중복 등록 제거** + - Goal 실행 `P18-R1`: `REV-P18-001`을 수정해 이번 계획 범위의 홈 추천 Swift 파일이 `SodaLive`와 `SodaLive-dev` target에 각각 정확히 한 번만 포함되게 한다. + - 시작 조건: + - 리뷰: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase18-final-project-build.md` + - 확정 항목: `REV-P18-001` + - 대상 파일: + - 수정: `SodaLive.xcodeproj/project.pbxproj` + - TDD 예외 사유: + - production 동작이 아니라 Xcode Compile Sources 메타데이터 중복을 정리하는 프로젝트 설정 Task다. + - 대체 검증 방법: + - 두 앱 target의 Sources phase별 file reference 포함 개수를 대조하고, 프로젝트 유효성 검사와 두 scheme 빌드 로그에서 대상 파일의 duplicate build warning이 사라졌는지 확인한다. + - 작업 내용: + - [x] `SodaLive` Sources phase에 두 번 등록된 `MainHomeView.swift`, `MainHomeRecommendationView.swift`, `MainHomeRecommendationViewModel.swift`, `MainHomeRankingView.swift`, `MainHomeFollowingView.swift`, `MainHomeLiveSection.swift`, `MainHomeBannerSection.swift`, `MainHomeActiveCreatorSection.swift`, `MainHomeRecentDebutCreatorSection.swift`, `MainHomeAiCharacterSection.swift`, `MainHomeBusinessInfoSection.swift`의 중복 build file 한 건씩을 확인한다. + - [x] 각 파일의 프로덕션 중복 build file과 Sources phase 참조 한 건만 제거하고 `SodaLive-dev` 등록 및 다른 기존 중복 항목은 변경하지 않는다. + - [x] `plutil -lint SodaLive.xcodeproj/project.pbxproj`와 `git diff --check`를 통과한다. + - [x] `SodaLive`, `SodaLive-dev` target에 위 11개 파일이 각각 1회 등록되어 있고 `xcodebuild -workspace "SodaLive.xcworkspace" -list`로 프로젝트 파싱이 성공하는지 확인한다. + - [x] 실제 등록 수와 검증 결과를 이 문서와 Phase 18 리뷰 리포트에 누적한다. + +- [x] **Task 18.4: 프로덕션 target에 남은 홈 추천 Sources 중복 등록 제거** + - Goal 실행 `P18-R2`: `REV-P18-002`를 수정해 `Task 18.3` 이후에도 두 번 남은 홈 추천 Swift 파일 11개의 프로덕션 Sources 등록을 각각 한 번으로 맞춘다. + - 시작 조건: + - 리뷰: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase18-final-project-build.md` + - 확정 항목: `REV-P18-002` + - 대상 파일: + - 수정: `SodaLive.xcodeproj/project.pbxproj` + - TDD 예외 사유: + - production 동작이 아니라 Xcode Compile Sources 메타데이터의 잔여 중복을 정리하는 프로젝트 설정 Task다. + - 대체 검증 방법: + - 두 앱 target의 Sources phase별 파일명 포함 수와 fresh DerivedData 빌드 경고를 수정 전후로 대조한다. + - 작업 내용: + - [x] 현재 `SodaLive` Sources phase에서 `MainHomeView.swift`, `MainHomeRecommendationView.swift`, `MainHomeRecommendationViewModel.swift`, `MainHomeRankingView.swift`, `MainHomeFollowingView.swift`, `MainHomeLiveSection.swift`, `MainHomeBannerSection.swift`, `MainHomeActiveCreatorSection.swift`, `MainHomeRecentDebutCreatorSection.swift`, `MainHomeAiCharacterSection.swift`, `MainHomeBusinessInfoSection.swift`가 각각 2회이고 `SodaLive-dev`에서는 각각 1회인 실패 상태를 확인한다. + - [x] 각 파일의 프로덕션 잔여 중복 build file과 Sources phase 참조 한 건만 제거하고 `SodaLive-dev` 및 범위 밖 파일은 변경하지 않는다. + - [x] `plutil -lint SodaLive.xcodeproj/project.pbxproj`와 `git diff --check`를 통과한다. + - [x] 두 target에서 대상 11개 파일이 각각 정확히 1회인지 대조하고 fresh DerivedData의 `SodaLive`, `SodaLive-dev` Debug 빌드를 실행한다. + - [x] 두 빌드 로그에서 대상 11개 파일의 `Skipping duplicate build file` 경고가 없는지 확인하고 실제 결과를 이 문서와 Phase 18 리뷰 리포트에 누적한다. + ### Phase 19: 문서와 검증 기록 갱신 - [x] **Task 19.1: PRD/계획 문서 갱신** @@ -488,6 +773,83 @@ - 실행 명령: `rg "### Phase [0-9]+:|- \\[[ x]\\] \\*\\*Task [0-9]+\\.[0-9]+:" docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` - 기대 결과: phase heading과 `Task N.N` 형식 체크박스가 검색된다. +- [x] **Task 19.2: 장르 크리에이터 미표시 기준 문서 정합성 보정** + - Goal 실행 `P19-R1`: `REV-P19-001`을 수정해 2026-07-12 장르 섹션 미표시 결정과 PRD·Phase 17 활성 기준을 일치시킨다. + - 시작 조건: + - 리뷰: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase19-documentation-records.md` + - 확정 항목: `REV-P19-001` + - 대상 파일: + - 수정: `docs/20260602_메인_홈_추천_UI_API_연동/prd.md` + - 수정: `docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` + - TDD 예외 사유: + - production 동작을 변경하지 않는 문서 정정 Task다. + - 대체 검증 방법: + - 현재 코드 조립 순서, PRD 화면 트리, Phase 17 기준과 2026-07-12 결정의 대조표 및 정적 검색으로 확인한다. + - 작업 내용: + - [x] PRD의 활성 화면 구성과 Phase 17 작업/기대 결과에서 장르 섹션 노출 요구가 남은 위치를 확인한다. + - [x] `genreCreators` 응답 모델 유지와 `MainHomeGenreCreatorSection` 미표시를 구분해 PRD와 활성 검증 기준을 정정한다. + - [x] 기존 Phase 13 구현 기록과 2026-06-30 검증 기록은 삭제하지 않고 후속 정정 기록을 누적한다. + - [x] `rg "장르의 크리에이터|MainHomeGenreCreatorSection|genreCreators" docs/20260602_메인_홈_추천_UI_API_연동 SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift`와 `git diff --check` 결과를 검증 기록에 남긴다. + +- [x] **Task 19.3: 활성 문서 코드 블록과 V2 컴포넌트 경로 정합성 보정** + - Goal 실행 `P19-R2`: `REV-P19-002`, `REV-P19-003`을 수정해 PRD Markdown 구조와 최신 V2 확장 텍스트 경로를 현재 구현에 맞춘다. + - 시작 조건: + - 리뷰: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase19-documentation-records.md` + - 확정 항목: `REV-P19-002`, `REV-P19-003` + - 대상 파일: + - 수정: `docs/20260602_메인_홈_추천_UI_API_연동/prd.md` + - 수정: `docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` + - TDD 예외 사유: + - production 동작을 변경하지 않는 Markdown 구조와 활성 파일 경로 정정 Task다. + - 대체 검증 방법: + - Markdown fence 쌍과 활성 경로의 실제 파일 존재 여부를 정적 검사한다. + - 작업 내용: + - PRD 응답 모델 예시의 누락·중첩된 코드 fence를 각 모델 단위로 닫아 이후 본문이 코드 블록으로 렌더링되지 않게 한다. + - PRD와 계획 상단의 활성 공용 컴포넌트 이름·경로를 `V2ExpandableTextView.swift`로 정정한다. + - 과거 Task와 검증 기록의 당시 경로는 이력으로 보존한다. + - 검증 기준: + - 실행 명령: PRD의 각 응답 모델 예시가 독립된 Markdown fence 여닫힘 쌍을 가지며 각 모델 heading과 설명이 코드 블록 밖에 있는지 확인한다. + - 실행 명령: `rg "V2/Component/Text/(ExpandableTextView|V2ExpandableTextView)\.swift" docs/20260602_메인_홈_추천_UI_API_연동/prd.md docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` + - 기대 결과: 활성 기준은 존재하는 `V2ExpandableTextView.swift`를 가리키고 과거 경로는 검증 이력에만 남아야 한다. + +- [x] **Task 19.4: 활성 API 타입명과 리뷰 정정 기록 보완** + - Goal 실행 `P19-R3`: `REV-P19-004`를 수정해 PRD의 추천 API 타입명과 Phase 11/21 후속 리뷰 기록을 현재 구현과 일치시킨다. + - 시작 조건: + - 리뷰: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase19-documentation-records.md` + - 확정 항목: `REV-P19-004` + - 대상 파일: + - 수정: `docs/20260602_메인_홈_추천_UI_API_연동/prd.md` + - 수정: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase11-recent-debut-creators.md` + - 수정: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase21-recent-activity-live-fallback.md` + - TDD 예외 사유: + - production 동작을 변경하지 않는 활성 문서 타입명과 append-only 리뷰 기록 정정 Task다. + - 대체 검증 방법: + - 실제 Swift 타입, Phase 11 카드 구현, Phase 21 destination 분기와 문서 문장을 정적 대조한다. + - 작업 내용: + - [x] PRD Goal의 `MainHomeApi`를 실제 활성 타입 `MainHomeRecommendationApi`로 정정한다. + - [x] Phase 11의 `CreatorProfileGrid` 재사용 확인 문구를 실제 전용 직사각형 카드 사용 판정과 일치하도록 후속 정정한다. + - [x] Phase 21의 `LIVE_REPLAY`가 라이브/creator fallback을 사용한다는 문구를 `targetId` 기반 콘텐츠 상세 이동 및 fallback 없음으로 후속 정정한다. + - [x] 기존 리뷰 기록은 삭제하지 않고 정정 기록을 누적하며, `rg`와 `git diff --check` 결과를 검증 기록에 남긴다. + +- [x] **Task 19.5: 최근 데뷔 섹션의 활성 계획 기준을 실제 전용 카드 구현과 맞춤** + - Goal 실행 `P19-R4`: `REV-P19-005`를 수정해 Phase 11의 완료 기준이 현재 `MainHomeRecentDebutCreatorItemView` 사용 판정과 모순되지 않게 한다. + - 시작 조건: + - 리뷰: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase19-documentation-records.md` + - 관련 Phase 리뷰: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase11-recent-debut-creators.md` + - 확정 항목: `REV-P19-005` + - 대상 파일: + - 수정: `docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` + - 수정: `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase11-recent-debut-creators.md` + - TDD 예외 사유: + - production 동작을 변경하지 않는 활성 계획·리뷰 정합성 보정 Task다. + - 대체 검증 방법: + - Phase 11 Task의 작업·기대 결과, 현재 전용 직사각형 카드 코드와 Phase 11 최신 리뷰 결론을 줄 단위로 대조한다. + - 작업 내용: + - [x] `Task 11.1`이 공용 원형 `CreatorProfileGrid` 재사용을 완료 조건으로 요구하지만 현재 코드와 최신 리뷰는 전용 직사각형 카드를 올바른 구현으로 판정하는 충돌을 확인한다. + - [x] 공용 컴포넌트 우선 원칙은 유지하되 Figma 카드 형태를 충족하지 못할 때 페이지 전용 `MainHomeRecentDebutCreatorItemView`를 사용하는 현재 결정을 활성 Task와 기대 결과에 명시한다. + - [x] 기존 Phase 11 완료 기록과 리뷰 정정 이력은 삭제하지 않고 후속 정정만 누적한다. + - [x] `rg "CreatorProfileGrid|MainHomeRecentDebutCreatorItemView" docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase11-recent-debut-creators.md SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeRecentDebutCreatorSection.swift`와 `git diff --check` 결과를 검증 기록에 남긴다. + ### Phase 20: 추천 커뮤니티 게시물 상세 진입 보정 - [x] **Task 20.1: PRD/계획 문서에 커뮤니티 게시물 상세 이동 요구사항 추가** @@ -628,8 +990,500 @@ - 실행 명령: `rg "LIVE_REPLAY.*(라이브룸|콘텐츠 상세)|onTapContent\\(targetId\\)" docs/20260602_메인_홈_추천_UI_API_연동/prd.md docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` - 기대 결과: 활성 요구사항과 미완료 구현 Task에는 `LIVE_REPLAY`의 콘텐츠 상세 이동만 명시되고, 과거 오류는 정정 기록에서만 설명된다. +### Phase 22: 모두 팔로우 확인 Dialog + +**Phase 결과:** 로그인 사용자는 메인 홈 추천 탭의 모든 `모두 팔로우하기` 진입점에서 Confirm Dialog를 확인한 뒤에만 여러 크리에이터를 팔로우하며, 취소 시 API 요청이 발생하지 않는다. + +**선행조건:** `MainHomeView`에서 전달된 모든 추천 탭 모두 팔로우 action이 `MainView.handleRecommendationFollowAllTap` 공통 진입점을 사용한다. + +**Phase 완료 조건:** Task 22.1~22.3을 완료하고 확인 전·취소·dimmed 영역 터치 API 0회, 확인 시 API 1회, 비로그인 기존 로그인 흐름 유지와 ko/en/ja 문구를 검증 기록에 누적한다. + +- [x] **Task 22.1: 인터뷰 결정사항을 PRD와 계획 문서에 반영** + - Goal: 모두 팔로우 확인 Dialog의 적용 범위, 인증 순서, 문구, 취소 동작과 완료 기준을 추정 없이 구현할 수 있게 고정한다. + - 대상 파일: + - 수정: `docs/20260602_메인_홈_추천_UI_API_연동/prd.md` + - 수정: `docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` + - 작업 내용: + - 메인 홈 추천 탭의 모든 공통 `모두 팔로우하기` 진입점에 적용한다. + - 비로그인 사용자는 기존처럼 로그인 화면으로 바로 이동하고, 로그인 후 자동 팔로우하지 않는다. + - 로그인 사용자는 Confirm Dialog의 `팔로우 하기`를 선택한 경우에만 기존 API를 호출한다. + - `취소`와 dimmed 영역 터치는 보류 action을 폐기하고 API를 호출하지 않는다. + - Dialog 제목, 설명, 확인 문구를 ko/en/ja로 확정하고 취소는 `I18n.Common.cancel`을 재사용한다. + - 현재 미노출 상태인 장르 크리에이터 섹션을 다시 표시하는 작업은 제외한다. + - 완료 증거: + - PRD 7.6, Success Criteria, Verification Plan, Decision Log와 Phase 22가 동일한 흐름과 문구를 사용한다. + - TDD 예외 사유: + - production 동작을 변경하지 않는 문서 갱신 Task다. + - 검증 기준: + - 실행 명령: `rg "모두 팔로우 확인 Dialog|모두 팔로우할까요|Follow all creators|すべてのクリエイターをフォローしますか|dimmed|자동 팔로우" docs/20260602_메인_홈_추천_UI_API_연동/prd.md docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` + - 기대 결과: 적용 범위, 인증 순서, 취소 동작과 ko/en/ja 문구가 두 문서에서 검색된다. + +- [x] **Task 22.2: 공통 모두 팔로우 Confirm Dialog와 I18n 구현** + - Goal: 기존 공통 모두 팔로우 callback에 최소 상태만 추가해 로그인 사용자의 명시적 확인 후에만 보류한 action을 실행한다. + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Main/MainView.swift` + - 수정: `SodaLive/Sources/I18n/I18n.swift` + - 확인: `SodaLive/Sources/V2/Main/Home/MainHomeView.swift` + - 확인: `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift` + - 확인: `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift` + - 확인: `SodaLive/Sources/V2/Component/Modal/SodaV2ActionModal.swift` + - 작업 내용: + - `handleRecommendationFollowAllTap`에서 기존 로그인 guard를 먼저 통과한 사용자에게만 `SodaV2ActionModal`을 표시한다. + - 보류한 모두 팔로우 action은 Dialog 확인 시 한 번 실행하고, 확인·취소·dimmed 영역 처리 후에는 즉시 비운다. + - Dialog 제목, 설명, 확인 문구는 `I18n.HomeRecommendation`의 ko/en/ja 값을 사용하고 취소는 `I18n.Common.cancel`을 재사용한다. + - 기존 `followAll(creatorIds:completionKey:)`의 완료, loading, 오류 toast와 중복 호출 방지 로직은 변경하지 않는다. + - `MainHomeView`와 섹션별 View의 callback 계층은 공통 진입점을 우회하지 않는 한 변경하지 않는다. + - 완료 증거: + - 공통 callback과 기존 `SodaV2ActionModal` 재사용만으로 확인 흐름이 연결되고, API/Repository/Request 계약 변경이 없다. + - TDD 예외 사유: + - 저장소에 테스트 번들 타깃이 없고 SwiftUI Dialog와 실제 네트워크 요청의 통합 흐름을 위한 신규 테스트 인프라 추가는 이번 최소 변경 범위를 벗어난다. + - 대체 검증 방법: + - 상태/action/I18n 연결 정적 검색, `SodaLive-dev` 빌드, 로그인/비로그인 수동 흐름과 네트워크 요청 횟수 확인으로 검증한다. + - 검증 기준: + - 실행 명령: `rg "handleRecommendationFollowAllTap|SodaV2ActionModal|pending.*FollowAll|followAll.*Confirm|I18n.HomeRecommendation|I18n.Common.cancel" SodaLive/Sources/V2/Main/MainView.swift SodaLive/Sources/I18n/I18n.swift` + - 기대 결과: 로그인 guard 뒤의 Dialog 표시, 보류 action, 확인/취소 처리와 다국어 키가 검색된다. + - 실행 명령: `rg "followRecommendedCreators|FollowRecommendedCreatorsRequest" SodaLive/Sources/V2/Main/Home/Recommendation` + - 기대 결과: 기존 모두 팔로우 API와 request 경로가 유지된다. + +- [x] **Task 22.3: 모두 팔로우 확인 흐름 Phase Gate** + - Goal: Confirm Dialog가 의도하지 않은 대량 팔로우를 차단하면서 기존 인증·완료·loading·오류 흐름을 유지하는지 최종 판정한다. + - 대상 파일: + - 확인: `SodaLive/Sources/V2/Main/MainView.swift` + - 확인: `SodaLive/Sources/V2/Main/Home/**` + - 확인: `SodaLive/Sources/I18n/I18n.swift` + - 확인: `SodaLive.xcodeproj/project.pbxproj` + - 완료 증거: + - 정적 검증, 프로젝트 유효성, `SodaLive-dev` 빌드와 아래 수동 확인 결과가 검증 기록에 누적된다. + - 검증 기준: + - 실행 명령: `git diff --check` + - 기대 결과: 출력 없이 성공한다. + - 실행 명령: `plutil -lint SodaLive.xcodeproj/project.pbxproj` + - 기대 결과: `OK` + - 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` + - 기대 결과: `BUILD SUCCEEDED` + - 수동 확인: + - 로그인 사용자가 `모두 팔로우하기`를 터치하면 Dialog가 표시되고 API 요청은 0회다. + - `취소` 또는 dimmed 영역을 터치하면 Dialog가 닫히고 API 요청은 0회다. + - `팔로우 하기`를 한 번 터치하면 Dialog가 닫히고 해당 섹션의 기존 API 요청이 1회 발생한다. + - 비로그인 사용자가 버튼을 터치하면 Dialog 없이 로그인 화면으로 이동하고, 로그인 완료 후 자동 팔로우하지 않는다. + - 기존 완료/호출 중 버튼 상태와 API 실패 toast가 유지된다. + - 앱 언어 ko/en/ja에서 제목, 설명, 확인, 취소 문구가 각각 표시된다. + ## 검증 기록 +### 2026-07-31 Task 5.4 독립 수정 확인 + +- 코드 확인: + - `LiveNowAllView`는 인증 성공 후 `handleLiveNowItemTap(roomId:isAdult:)`를 재호출해 민감 콘텐츠 설정 guard를 다시 통과함 + - `CreatorChannelView`는 인증 성공 후 `handleTalkTap()`을 재호출해 민감 콘텐츠 설정 guard를 다시 통과함 +- 검증: + - 인증·설정 4개 조합과 인증 미완료 상태 self-check — `task-5.4-independent-self-check=passed` + - `git diff --check`, `git diff --cached --check` — 출력 없이 성공 + - `plutil -lint SodaLive.xcodeproj/project.pbxproj` — `OK` + - `SodaLive-dev`, `SodaLive` Debug `generic/platform=iOS` 빌드 — 모두 종료 코드 0 +- 추가 점검: + - `auth == false`와 `isAdultContentVisible`을 함께 사용하는 Swift 화면을 재검색하고 현재 `ContentView`에서 도달 가능한 `MainView`, `LiveNowAllView`, `CreatorChannelView`의 guard 재진입을 확인 + - 활성 앱 라우팅 범위에서 신규 확정 발견 사항 없음 + - 비활성 레거시 `HomeView`, `HomeTabView`, `ChatTabView`는 기존 범위 제외 상태 유지 +- 범위: + - 이번 점검에서는 production Swift 코드를 추가 수정하지 않음 + +### 2026-07-31 리뷰 보완 Task 5.4 처리 + +- 목적: `REV-P5-003` 보완 +- 수행 내용: + - `LiveNowAllView.handleLiveNowItemTap`의 인증 후 `pendingAction`을 `openLiveDetail(roomId:)` 직접 실행에서 `handleLiveNowItemTap(roomId:isAdult:)` 재호출로 변경 + - `CreatorChannelView.handleTalkTap`의 인증 후 `pendingAction`을 `startChat(characterId:)` 직접 실행에서 `handleTalkTap()` 재호출로 변경 + - 공용 guard 계층을 새로 만들지 않고 기존 로그인·국가·본인인증·민감 콘텐츠 설정 분기와 route를 유지 +- TDD 예외/대체 검증: + - 앱 테스트 번들 타깃이 없어 Bootpay 본인인증 callback과 전역 route를 직접 실행하는 영구 통합 test는 추가하지 않음 + - Swift 상태 전이 self-check, 정적 검색, 공백 오류, 프로젝트 유효성, 두 scheme Debug 빌드로 대체 검증 +- 검증: + - `swift -e '<기존 LiveNowAllView/CreatorChannelView pendingAction 상태 전이 self-check>'` 실행, 기존 방식은 `RED live old path bypassed settings: route=true settings=false`로 실패 재현 + - `swift -e '<수정 후 LiveNowAllView/CreatorChannelView pendingAction 상태 전이 self-check>'` 실행, `task-5.4-guard-self-check=passed` 확인 + - `rg -n "pendingAction = \\{|handleLiveNowItemTap\\(roomId: roomId, isAdult: isAdult\\)|handleTalkTap\\(\\)|openLiveDetail\\(roomId: roomId\\)|startChat\\(characterId: characterId\\)|contentViewSettings|isAdultContentVisible" "SodaLive/Sources/Live/Now/All/LiveNowAllView.swift" "SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift"` 실행, 인증 후 guard 재진입과 기존 최종 route 유지 확인 + - `git diff --check` 실행, 출력 없이 성공 + - `plutil -lint "SodaLive.xcodeproj/project.pbxproj"` 실행, `OK` + - `rg -n -- "- \\[ \\]" "docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md"` 실행, 미완료 Task 검색 결과 없음 + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -destination "generic/platform=iOS" -derivedDataPath "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataTask54Dev" build` 최초 병렬 실행은 10분 제한으로 `BUILD INTERRUPTED`, 동일 DerivedData 재시도는 `BUILD SUCCEEDED` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug -destination "generic/platform=iOS" -derivedDataPath "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataTask54Prod" build` 최초 병렬 실행은 10분 제한으로 `BUILD INTERRUPTED`, 동일 DerivedData 재시도는 `BUILD SUCCEEDED` +- 잔여 확인: + - 실제 기기/시뮬레이터의 Bootpay 본인인증 완료 수동 확인은 이번 환경에서 수행하지 못해 상태 전이 self-check와 앱 빌드로 대체함 + - `SodaLive` 빌드의 범위 밖 V2 파일 duplicate Sources warning은 기존 상태로 남아 있으며 이번 Task에서 변경하지 않음 + +### 2026-07-31 Task 5.3/9.5 수정 확인 및 활성 guard 추가 점검 + +- 수정 확인: + - `MainView.performRecommendationDetailAction(requiresAdultContent: true)`는 인증 성공 후 동일 guard를 재진입해 민감 콘텐츠 설정 비활성 시 설정 화면으로 이동하고 상세 route를 실행하지 않음 + - `BannerCarousel.wrapIfNeeded()`는 예약 당시 `displayIndex`와 `itemsRevision`이 모두 유지된 경우에만 지연 reset을 실행해 반대 방향 이동과 같은 개수의 데이터 교체를 덮어쓰지 않음 +- self-check: + - 인증 전후 4개 상태 조합과 인증 미완료 상태 — `auth-guard-self-check=passed` + - 첫·마지막 복제 경계, 반대 방향 이동, 같은 개수 데이터 교체, 반복 같은 방향 입력 — `banner-reset-self-check=passed` +- 정적·빌드 검증: + - `git diff --check`, `git diff --cached --check` — 출력 없이 성공 + - `plutil -lint SodaLive.xcodeproj/project.pbxproj` — `OK` + - `SodaLive-dev`, `SodaLive` Debug `generic/platform=iOS` 빌드 — 모두 종료 코드 0 +- 추가 확정 항목: + - `REV-P5-003` High — 현재 `ContentView`에서 실제 라우팅되는 `LiveNowAllView`의 성인 라이브와 `CreatorChannelView`의 AI 대화가 인증 성공 시 최종 route를 직접 실행해 민감 콘텐츠 보기 설정을 우회함 + - Phase 5 `Task 5.4` / `P5-R3` 신규 추가 +- 범위: + - 현재 앱 route에서 사용되지 않는 레거시 `HomeView`, `HomeTabView`, `ChatTabView`의 동일 패턴은 이번 Task 대상에서 제외 + - 이번 점검에서는 production Swift 코드를 추가 수정하지 않음 + +### 2026-07-31 리뷰 보완 Task 5.3/9.5 처리 + +- 목적: `REV-P5-002`, `REV-P9-001` 잔여 보완 +- 수행 내용: + - `MainView.performRecommendationDetailAction(requiresAdultContent: true)`에서 본인인증 필요 시 최종 상세 route가 아니라 동일 guard를 재진입하는 closure를 `pendingAction`에 저장하도록 수정 + - `BannerCarousel.wrapIfNeeded()`의 지연 reset이 예약 당시 `displayIndex`와 `itemsRevision`이 유지된 경우에만 실행되도록 보정 +- TDD 예외/대체 검증: + - 앱 테스트 번들 타깃이 없어 Bootpay callback과 SwiftUI gesture/timer의 영구 unit/UI test는 추가하지 않음 + - Swift 상태 전이 self-check, 정적 검색, 공백 오류, 프로젝트 유효성, `SodaLive-dev` Debug 빌드로 대체 검증 +- 검증: + - `swift -e '<기존 본인인증 후 pendingAction 상태 전이 self-check>'` 실행, 기존 방식은 `detail=true settings=false`로 RED 실패 재현 + - `swift -e '<수정 후 본인인증 후 pendingAction 상태 전이 self-check>'` 실행, `settings=false`는 설정 이동·상세 미실행, `settings=true`는 상세 실행 통과 확인 + - `swift -e '<기존 배너 예약 reset 상태 전이 self-check>'` 실행, 반대 방향 이동 후 예약 reset이 `3`을 `1`로 덮어쓰는 RED 실패 재현 + - `swift -e '<수정 후 배너 예약 reset 상태 전이 self-check>'` 실행, 반대 방향 이동과 같은 개수의 `items` 변경은 덮어쓰지 않고 같은 경계는 정상 reset되는 것 확인 + - `rg -n "pendingAction = \{|performRecommendationDetailAction\(requiresAdultContent: true, action: action\)|itemsRevision|scheduledIndex|scheduledRevision" SodaLive/Sources/V2/Main/MainView.swift SodaLive/Sources/V2/Component/Banner/BannerCarousel.swift` 실행, guard 재진입과 조건부 reset 확인 + - `git diff --check` 실행, 출력 없이 성공 + - `plutil -lint "SodaLive.xcodeproj/project.pbxproj"` 실행, `OK` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -destination "generic/platform=iOS" -derivedDataPath "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataTask5395Dev" build` 실행, 10분 도구 제한으로 asset/codesign 단계에서 `BUILD INTERRUPTED` + - 동일 DerivedData `xcodebuild -quiet ... generic/platform=iOS ...` 재시도와 `CODE_SIGNING_ALLOWED=NO` 재시도도 10분 도구 제한으로 `BUILD INTERRUPTED` + - `xcodebuild -quiet ... -destination "generic/platform=iOS Simulator" ... CODE_SIGNING_ALLOWED=NO build` 실행, SPM `Promises` 컴파일 중 비정상 종료되어 `BUILD FAILED` + - `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLive-sim-build-20260731-200318.log`에서 `BUILD INTERRUPTED` 확인 +- 수동 확인: + - 실제 기기/시뮬레이터의 Bootpay 본인인증 완료와 빠른 경계 연속 스와이프 시각 확인은 이번 환경에서 직접 수행하지 못해 상태 전이 self-check와 정적 검증으로 대체함 +- 차단: + - `SodaLive-dev` Debug 빌드 성공 증거는 확보하지 못함. iPhoneOS 빌드는 10분 도구 제한으로 반복 중단됐고, Simulator 빌드는 SPM `Promises` 컴파일 단계에서 실패함. + +### 2026-07-31 Phase 1~22 리뷰 재점검 및 후속 Task 전환 + +- 목적: 현재 working tree의 Phase별 리뷰 판정과 실제 production 코드·활성 계획을 다시 대조하고, 남은 확정 결함을 이어서 실행 가능한 Task로 전환 +- Phase별 결과: + - Phase 1~4, 6~8, 10~22: 기존 Phase별 최신 판정을 유지하며 이번 재점검에서 신규 확정 항목 없음 + - Phase 5: `REV-P5-002` High 확정 — 한국 사용자에게 본인인증이 먼저 필요한 경우 인증 성공 후 민감 콘텐츠 보기 설정 guard를 건너뛰고 AI 캐릭터 상세 route를 직접 실행함 + - Phase 9: 기존 `REV-P9-001`의 index 범위 정규화만으로는 예약된 경계 reset이 반대 방향 이동 후 정상 index를 덮어쓰는 상태를 막지 못해 수정 미완료로 재판정 +- 계획 전환: + - Phase 5 `Task 5.3` / `P5-R2` 신규 추가 + - Phase 9는 동일 경계 상태를 소유하는 기존 미완료 `Task 9.5` / `P9-R1`의 재현·완료 조건을 보강하고 중복 Task는 추가하지 않음 +- 재현: + - `swift -module-cache-path /private/tmp/sodalive-review-module-cache-auth -e '<본인인증 후 pendingAction 상태 전이>'` — `detail=true settings=false`, 설정 guard 우회 재현 + - `swift -module-cache-path /private/tmp/sodalive-review-module-cache-banner -e '<배너 경계 지연 reset 상태 전이>'` — 반대 방향 이동 후 `beforeDelayedReset=3`, 예약 reset 뒤 `afterDelayedReset=1, expected=3` 재현 +- 검증: + - `xcodebuild -workspace SodaLive.xcworkspace -list` — workspace와 `SodaLive`, `SodaLive-dev` 스킴 인식 확인 + - `xcodebuild -quiet -workspace SodaLive.xcworkspace -scheme SodaLive-dev -configuration Debug -destination generic/platform=iOS -derivedDataPath /private/tmp/SodaLiveDerivedDataReviewFinal-dev build` — 종료 코드 0 + - `xcodebuild -quiet -workspace SodaLive.xcworkspace -scheme SodaLive -configuration Debug -destination generic/platform=iOS -derivedDataPath /private/tmp/SodaLiveDerivedDataReviewFinal-dev build` — 종료 코드 0 + - `plutil -lint SodaLive.xcodeproj/project.pbxproj` — `OK` + - `git diff --check`, `git diff --cached --check` — 출력 없이 성공 + - Phase별 리포트 22개와 각 리포트의 필수 1~9절 존재 확인 +- 기존 Task 정리: + - Phase 4 `Task 4.2` / `P4-R1`은 추천 ViewModel의 직접 `print(` 제거, `ERROR_LOG` 사용과 `SodaLive-dev` 빌드 성공을 확인해 완료 처리 +- 범위: + - 리뷰 리포트와 후속 계획만 갱신했으며 production Swift 코드는 수정하지 않음 + - 기존 완료 체크와 과거 검증 기록은 삭제하거나 되돌리지 않음 + +### 2026-07-31 리뷰 보완 Task 4.2/9.5/19.5 처리 + +- 목적: `REV-P4-001`, `REV-P9-001`, `REV-P19-005` 보완 +- 수행 내용: + - `MainHomeRecommendationViewModel.fetchRecommendations()`의 추천 응답 디코딩 `catch`에서 `print(error)`를 `ERROR_LOG(error.localizedDescription)`로 교체 + - `BannerCarousel.wrapIfNeeded()`에서 `displayIndex`를 `0...items.count + 1` 복제 범위로 정규화해 경계 reset 지연 중 연속 입력이 범위 밖 index를 지속시키지 않도록 보정 + - Phase 11 활성 Task 기준에 공용 원형 creator widget 우선 검토와 Figma 직사각형 카드 미충족 시 전용 `MainHomeRecentDebutCreatorItemView` 사용 결정을 명시 +- TDD 예외/대체 검증: + - 앱 테스트 번들 타깃이 없어 ViewModel 로그 호출, SwiftUI gesture/timer, 문서 정합성에 대한 영구 unit/UI test는 추가하지 않음 + - 정적 실패 근거 재현, Swift 상태 전이 self-check, 정적 검색, 공백 오류, 프로젝트 유효성, `SodaLive-dev` Debug 빌드로 대체 검증 +- 검증: + - `rg -n "print\(|ERROR_LOG|func fetchRecommendations|catch" "SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift" "SodaLive/Sources/Utils/Log.swift"` 실행, 추천 ViewModel 범위의 직접 `print(` 제거와 `ERROR_LOG` 사용 확인 + - `swift -e ''` 실행, 기존 조건의 `items.count + 2`/`-1` 실패와 보정 후 `1...items.count` 복구 self-check 통과 확인 + - `rg -n "displayIndex = min\(max\(displayIndex, 0\), items.count \+ 1\)|wrapIfNeeded|currentPage" "SodaLive/Sources/V2/Component/Banner/BannerCarousel.swift"` 실행, index 정규화와 기존 page 계산 유지 확인 + - `rg -n "CreatorProfileGrid|MainHomeRecentDebutCreatorItemView" "docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md" "docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase11-recent-debut-creators.md" "SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeRecentDebutCreatorSection.swift"` 실행, 활성 계획·리뷰·코드 기준 일치 확인 + - `plutil -lint "SodaLive.xcodeproj/project.pbxproj"` 실행, `OK` + - `git diff --check` 실행, 출력 없이 성공 + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -destination "generic/platform=iOS" -derivedDataPath "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataTask4295195" build` 실행 2회, Pods/SPM 및 app resource 처리 중 10분 도구 제한으로 `BUILD INTERRUPTED` +- 차단: + - `SodaLive-dev` Debug 빌드 성공 증거와 실제 기기/시뮬레이터 빠른 경계 연속 스와이프 수동 확인은 아직 확보하지 못함 +- 수동 확인: + - 실제 기기/시뮬레이터의 빠른 경계 연속 스와이프 시각 확인은 이번 환경에서 수행하지 못해 정적 self-check와 빌드 결과로 대체함 + +### 2026-07-31 Phase 1~22 최종 코드 리뷰 및 신규 Task 전환 + +- 목적: 현재 working tree의 PRD, 계획, Phase별 리포트, production 코드와 Xcode target 상태를 다시 대조하고 남은 확정 항목을 실행 가능한 후속 Task로 전환 +- Phase별 리포팅: + - `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase1-*.md`부터 `phase22-*.md`까지 Phase별 22개 리포트를 유지하고 최신 판정을 해당 소유 리포트에 누적 +- 신규 확정 항목: + - `REV-P4-001` Low: 신규 추천 ViewModel의 응답 디코딩 `catch`가 프로젝트 로깅 규칙의 `ERROR_LOG` 대신 `print(error)`를 사용함 + - `REV-P9-001` Medium: 무한 캐러셀 복제 경계의 지연 reset 중 같은 방향 스와이프가 다시 끝나면 `displayIndex`가 유효 범위를 벗어나 빈 배너 상태가 지속될 수 있음 + - `REV-P19-005` Low: Phase 11 활성 Task는 `CreatorProfileGrid` 재사용을 완료 조건으로 두지만 현재 코드와 최신 리뷰는 Figma 직사각형 전용 카드를 올바른 구현으로 판정함 +- 계획 전환: + - Phase 4 `Task 4.2` / `P4-R1` + - Phase 9 `Task 9.5` / `P9-R1` + - Phase 19 `Task 19.5` / `P19-R4` +- 기존 항목 재판정: + - `REV-P2-001`, `REV-P5-001`, `REV-P6-001`, `REV-P10-001`, `REV-P12-001`, `REV-P15-001`, `REV-P15-002`, `REV-P16-001`, `REV-P16-002`, `REV-P18-001`, `REV-P18-002`, `REV-P19-001`~`REV-P19-004`는 현재 코드·문서에서 수정 완료 상태 유지 + - Phase 1~3, 5~8, 10~18, 20~22에서는 이번 재점검으로 추가 확정된 production 기능 결함 없음. Phase 11 production 구현은 유지하고 활성 문서 충돌만 Phase 19가 소유 +- 검증: + - `git diff --check`, `git diff --cached --check` — 성공, 출력 없음 + - `plutil -lint SodaLive.xcodeproj/project.pbxproj` — `OK` + - Phase 1~22 리뷰 파일의 샘플 필수 절 `1`~`9` 존재 여부 — 22개 모두 충족 + - PRD Markdown fence 수 — 10개로 짝수이며 활성 응답 예시 구조 확인 + - 제외 섹션/Figma URL/기존 `HomeApi` 추천 endpoint 정적 검색 — 검색 결과 없음 + - `xcodebuild -workspace SodaLive.xcworkspace -list` — 종료 코드 0, `SodaLive`, `SodaLive-dev` scheme 확인 + - `xcodebuild -quiet -workspace SodaLive.xcworkspace -scheme SodaLive-dev -configuration Debug -destination generic/platform=iOS -derivedDataPath /private/tmp/SodaLiveDerivedDataReview20260731Dev build` — 종료 코드 0 + - 동일 clean DerivedData의 `SodaLive` Debug 빌드 — 종료 코드 0 + - 두 빌드에서 `REV-P18-002` 대상 홈 추천 파일 11개의 duplicate build warning 없음. 프로덕션 target의 다른 V2 파일 기존 duplicate warning은 이번 계획 범위 밖으로 유지 +- 범위: + - 이번 리뷰에서는 production 코드 결함을 수정하지 않고 Phase별 리뷰 리포트와 후속 계획만 갱신 + - 기존 완료 체크와 검증 기록은 삭제·되돌리지 않고 신규 Task와 판정을 누적 + +### 2026-07-31 리뷰 보완 Task 10.2/18.4 처리 + +- 목적: `REV-P10-001`, `REV-P18-002` 보완 +- 수행 내용: + - `MainHomeActiveCreatorSection`의 최근 활동 상대 시간 표시를 공용 `DateParser.relativeTimeText(fromUTC:fallback:)` 호출로 교체하고 중복 계산 helper를 제거 + - `SodaLive.xcodeproj/project.pbxproj`의 `SodaLive` Sources phase에서 홈 추천 범위 11개 Swift 파일의 잔여 중복 build file 참조를 제거 +- TDD 예외/대체 검증: + - production 동작을 새로 추가하지 않는 구조 정리와 Xcode Sources 메타데이터 정리 작업이며 앱 테스트 번들 타깃이 없어 신규 테스트는 추가하지 않음 + - 공용 함수 연결 정적 검색, target별 Sources 등록 수, 프로젝트 유효성, 공백 오류, 두 scheme 빌드와 duplicate warning 검색으로 대체 검증 +- 검증: + - `rg -n "dateComponents|timeIntervalSince|relativeTimeText|relativeActivityAtText" "SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeActiveCreatorSection.swift" "SodaLive/Sources/Common/DateParser.swift"` 실행, 추천 섹션은 `DateParser.relativeTimeText` 호출만 남고 중복 helper는 검색되지 않음 + - 홈 추천 범위 11개 Swift 파일 Sources 등록 수 대조 — `SodaLive=1`, `SodaLive-dev=1` + - `plutil -lint "SodaLive.xcodeproj/project.pbxproj"` 실행, `OK` + - `git diff --check` 실행, 출력 없이 성공 + - `xcodebuild -quiet -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -destination "generic/platform=iOS" -derivedDataPath "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataTask184DevRetry" build` 실행, 종료 코드 0 + - `xcodebuild -quiet -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug -destination "generic/platform=iOS" -derivedDataPath "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataTask184Prod" build` 실행, 종료 코드 0 + - 두 빌드 로그에서 `REV-P18-002` 대상 11개 홈 추천 파일의 duplicate build warning은 검색되지 않음. 기존 범위 밖 `MainHomeRankingViewModel.swift`, `MainHomeFollowingViewModel.swift` duplicate warning은 남아 있어 이번 Task에서 변경하지 않음 + - 첫 `SodaLive-dev` 빌드 시도는 10분 제한으로 `BUILD INTERRUPTED`됐고, 위 결과는 동일 변경을 별도 DerivedData에서 다시 검증한 결과임 + +### 2026-07-31 Phase 1~22 최종 재점검 및 후속 Task 전환 + +- 목적: 현재 working tree의 Phase별 리포트, PRD, 계획, 코드와 프로젝트 등록을 다시 대조해 기존 보완 판정의 정확성을 확인 +- 확정 항목: + - `REV-P10-001` Low: `MainHomeActiveCreatorSection.relativeActivityAtText(now:)`가 공용 `DateParser.relativeTimeText(fromUTC:fallback:now:)`와 동일한 상대 시간 계산을 복제함 + - `REV-P18-002` Low: `Task 18.3` 완료 기록과 달리 대상 홈 추천 Swift 파일 11개가 현재 `SodaLive=2`, `SodaLive-dev=1`로 등록되어 프로덕션 duplicate build warning이 남음 +- 계획 전환: + - Phase 10 `Task 10.2` / `P10-R1`: 공용 `DateParser.relativeTimeText` 재사용 및 중복 계산 제거 + - Phase 18 `Task 18.4` / `P18-R2`: 프로덕션 Sources 잔여 중복 한 건씩 제거 및 두 scheme fresh 빌드 검증 +- 기존 항목 재판정: + - `REV-P2-001`, `REV-P5-001`, `REV-P6-001`, `REV-P12-001`, `REV-P15-001`, `REV-P15-002`, `REV-P16-001`, `REV-P16-002`, `REV-P19-001`~`REV-P19-004`는 현재 코드·문서에서 수정 완료 상태 유지 + - Phase 1~9, 11~17, 19~22에서는 이번 재점검으로 추가 확정된 기능 결함 없음 +- 검증: + - `git diff --check` — 성공, 출력 없음 + - `plutil -lint SodaLive.xcodeproj/project.pbxproj` — `OK` + - 제외 섹션/Figma URL/기존 `HomeApi` 추천 endpoint 정적 검색 — 검색 결과 없음 + - PBX Sources phase 대조 — 대상 11개 파일 모두 `SodaLive=2`, `SodaLive-dev=1` + - 격리 DerivedData 빌드 — `SodaLive`와 `SodaLive-dev` 모두 종료 코드 0 + - 빌드 경고 대조 — `SodaLive`에서는 대상 11개 파일의 duplicate build warning이 모두 재현됐고 `SodaLive-dev`에서는 대상 경고 없음 +- 범위: + - 이번 리뷰에서는 production Swift 코드와 프로젝트 중복 등록을 수정하지 않고 후속 Task만 추가 + - 기존 완료 체크와 과거 검증 기록은 삭제·되돌리지 않고 후속 판정을 누적 + +### 2026-07-31 리뷰 보완 Task 18.3/19.4 처리 + +- 목적: `REV-P18-001`, `REV-P19-004` 보완 +- 수행 내용: + - `SodaLive.xcodeproj/project.pbxproj`에서 홈 추천 범위 11개 Swift 파일의 중복 Compile Sources 등록을 정리 + - PRD Goal의 활성 추천 API 타입명을 `MainHomeRecommendationApi`로 정정 + - Phase 11/21 리뷰의 오기 기록을 삭제하지 않고 append-only 후속 정정 완료 기록 추가 +- TDD 예외/대체 검증: + - production 동작 변경이 아니라 Xcode Sources 메타데이터와 문서 정정 작업이므로 신규 테스트는 추가하지 않음 + - target별 Sources 등록 수, 프로젝트 파싱, 정적 검색, 공백 오류 검증으로 대체 +- 검증: + - 홈 추천 범위 11개 Swift 파일 Sources 등록 수 대조 — `SodaLive=1`, `SodaLive-dev=1` + - `plutil -lint "SodaLive.xcodeproj/project.pbxproj"` 실행, `OK` + - `xcodebuild -workspace "SodaLive.xcworkspace" -list` 실행, 프로젝트 파싱 성공 및 `SodaLive`, `SodaLive-dev` scheme 확인 + - `xcodebuild -quiet -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -derivedDataPath "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataTask183DevRetry" build` 실행, 종료 코드 0 + - `xcodebuild -quiet -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug -derivedDataPath "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataTask183ProdFinal" build` 실행, 종료 코드 0 + - 두 빌드 로그에서 `REV-P18-001` 대상 11개 홈 추천 파일의 duplicate build warning은 검색되지 않음. 기존 범위 밖 V2 파일 duplicate warning은 남아 있어 이번 Task에서 변경하지 않음 + - `rg -n "MainHomeApi|MainHomeRecommendationApi|CreatorProfileGrid|MainHomeRecentDebutCreatorItemView|LIVE_REPLAY|liveReplay|targetId" "docs/20260602_메인_홈_추천_UI_API_연동/prd.md" "docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase11-recent-debut-creators.md" "docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase21-recent-activity-live-fallback.md"` 실행, 활성 타입명과 append-only 정정 기록 확인 + - `git diff --check` 실행, 출력 없이 성공 +- 부수 정리: + - 빌드 과정에서 SwiftPM이 자동 갱신한 `SodaLive.xcworkspace/xcshareddata/swiftpm/Package.resolved`는 이번 Task 범위 밖 변경이므로 원래 내용으로 되돌림 + +### 2026-07-31 Phase 1~22 최종 리뷰 점검 및 후속 Task 전환 + +- 목적: Phase별 기존 리뷰와 보완 구현을 최신 working tree에서 다시 대조하고, 누락된 프로젝트·문서 결함을 실행 가능한 후속 Task로 전환 +- 확정 항목: + - `REV-P18-001` Low: `SodaLive` target에서 이번 계획 범위의 홈 추천 Swift 파일 11개가 각각 두 번 Sources 등록되어 프로덕션 빌드에 duplicate build warning이 발생함 + - `REV-P19-004` Low: PRD의 활성 추천 API 타입명과 Phase 11/21 후속 리뷰 기록 일부가 현재 Swift 구현과 다름 +- 계획 전환: + - Phase 18 `Task 18.3` / `P18-R1`: 대상 11개 파일의 프로덕션 중복 Sources 등록 제거 및 두 scheme 빌드 검증 + - Phase 19 `Task 19.4` / `P19-R3`: PRD API 타입명과 append-only 리뷰 정정 기록 보완 +- 기존 항목 재판정: + - `REV-P16-002`의 `V2ExpandableTextView.swift`는 두 앱 target에 각각 한 번만 등록되고 최신 프로덕션 빌드에서 해당 중복 경고가 없어 수정 완료 유지 + - Phase 1~6, 8~17, 20~22 production 코드에서는 이번 최종 점검으로 추가 확정된 기능 결함 없음 +- 검증: + - `git diff --check` — 성공, 출력 없음 + - `plutil -lint SodaLive.xcodeproj/project.pbxproj` — `OK` + - 제외 섹션/Figma URL/기존 `HomeApi` 추천 endpoint 정적 검색 — 검색 결과 없음 + - PBX Sources phase 대조 — `REV-P18-001` 대상 11개 파일은 `SodaLive=2`, `SodaLive-dev=1` + - `xcodebuild -quiet -workspace SodaLive.xcworkspace -scheme SodaLive -configuration Debug -derivedDataPath /private/tmp/SodaLiveDerivedDataReviewCurrentProd build` — 종료 코드 0, 빌드 성공 및 대상 duplicate warning 재현 + - 최신 `SodaLive-dev` 빌드 재실행은 10분 이상 종료되지 않아 중단했으며, 동일 dev target의 직전 현재 변경군 `BUILD SUCCEEDED` 기록을 유지 +- 범위: + - production 코드와 프로젝트 중복 등록은 수정하지 않고 후속 Task만 추가 + - 기존 완료 기록과 리뷰 판정은 삭제하지 않고 Phase별 리포트에 후속 판정을 누적 + +### 2026-07-31 리뷰 보완 Task 6.4/15.3/16.3/19.3 처리 + +- 목적: 2026-07-31 재점검에서 확정된 `REV-P6-001`, `REV-P15-002`, `REV-P16-002`, `REV-P19-002`, `REV-P19-003` 보완 +- 수행 내용: + - `MainHomeView`의 추천/랭킹/팔로잉 탭 제목을 기존 `I18n.MainContentRecommendation.tabTitle`, `I18n.MainContentRanking.tabTitle`, `I18n.HomeFollowing.tabTitle`로 교체 + - `CommunityPostCard`에서 `imageUrl == nil && price > 0 && existOrdered == false`인 게시물도 기존 `onTapPurchase` callback으로 진입하는 구매 affordance 표시 + - `SodaLive` target에 중복 등록된 `V2ExpandableTextView.swift in Sources` 한 건 제거, `SodaLive-dev` target 등록은 유지 + - PRD 응답 모델 예시의 Markdown fence를 모델 단위로 분리하고 활성 V2 확장 텍스트 경로를 `V2ExpandableTextView.swift`로 정정 +- TDD 예외/대체 검증: + - 저장소에 앱 테스트 번들 타깃이 없어 SwiftUI 렌더링·버튼 callback을 직접 실행하는 영구 unit/UI test는 추가하지 않음 + - 변경 전 정적 RED 체크에서 한글 탭 제목, 이미지 없는 유료 미구매 구매 affordance 부재, PBX 중복 등록, 활성 문서의 이전 경로를 재현 +- 검증: + - `rg -n 'MainContentRecommendation\.tabTitle|MainContentRanking\.tabTitle|HomeFollowing\.tabTitle' SodaLive/Sources/V2/Main/Home/MainHomeView.swift` 실행, 기존 I18n 키 연결 확인 + - `rg -n 'hasImage|isLocked|lockedTextPurchaseButton|paidLockOverlay|purchaseCommunityPost|onTapPurchase' SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift SodaLive/Sources/V2/Main/Home/Recommendation` 실행, 이미지 유무와 무관한 유료 미구매 구매 경로 확인 + - `rg -n 'V2ExpandableTextView\.swift in Sources|C922F89D' SodaLive.xcodeproj/project.pbxproj` 실행, build file은 `C922F89B`, `C922F89C` 두 건만 남고 중복 `C922F89D` 제거 확인 + - `rg -n '^```|#### RecommendationBannerResponse|#### HomePopularCommunityPostItem|V2/Component/Text/(ExpandableTextView|V2ExpandableTextView)\.swift|V2ExpandableTextView' docs/20260602_메인_홈_추천_UI_API_연동/prd.md docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` 실행, PRD fence 10개와 활성 `V2ExpandableTextView.swift` 경로 확인 + - `plutil -lint SodaLive.xcodeproj/project.pbxproj` 실행, `OK` + - `git diff --check` 실행, 출력 없이 성공 + - `SodaLive-dev` Debug 전체 빌드는 일반 실행 2회와 백그라운드 실행 1회가 모두 Pods/SDK 컴파일 중 10분 도구 제한을 초과해 완료 증거를 확보하지 못함. 백그라운드 프로세스 `20992`는 종료 처리함 + - Oracle 리뷰어 게이트 실행, 네 보완 Task에 대한 차단/중요 findings 없음 판정 + - `lsp_diagnostics`는 개별 Swift 파일 단독 분석에서 기존 프로젝트 타입과 외부 모듈(`Kingfisher`)을 찾지 못해 false positive를 반환했으므로 실제 컴파일 검증은 `xcodebuild` 결과를 기준으로 판단 +- 미실행 수동 확인: + - 실제 기기/시뮬레이터에서 ko/en/ja 탭 제목과 이미지 없는 유료 미구매 카드 구매 버튼 탭의 API 1회 호출은 아직 수행하지 못함 + +### 2026-07-31 Phase 1~22 후속 통합 리뷰 및 신규 Task 전환 + +- 목적: 2026-07-30 통합 리뷰 후 보완된 구현과 Phase 22 수동 완료 상태를 현재 PRD·계획·코드·프로젝트에 다시 대조하고, 새로 확정한 항목을 소유 Phase의 실행 가능한 Task로 전환 +- Phase별 리포팅: + - `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase1-*.md`부터 `phase22-*.md`까지 22개 리포트에 2026-07-31 재점검 기록을 각각 누적 +- 기존 리뷰 항목 재판정: + - `REV-P2-001`, `REV-P5-001`, `REV-P12-001`, `REV-P15-001`, `REV-P16-001`, `REV-P19-001`은 현재 구현·문서에서 수정 완료 확인 +- 신규 확정 항목: + - `REV-P6-001` Medium: 홈 추천/랭킹/팔로잉 탭 제목이 기존 I18n 키를 사용하지 않고 한글 문자열로 고정됨 + - `REV-P15-002` Medium: `imageUrl == nil`인 유료 미구매 게시물은 잠금·구매 Button이 렌더링되지 않아 기존 구매 API로 진입할 수 없음 + - `REV-P16-002` Low: `V2ExpandableTextView.swift`가 프로덕션 `SodaLive` Sources에 두 번 등록됨 + - `REV-P19-002` Low: PRD 7.1 응답 예시의 Markdown code fence가 중첩·누락됨 + - `REV-P19-003` Low: PRD와 계획의 활성 기준이 이름 변경 전 V2 `ExpandableTextView.swift` 경로를 가리킴 +- 계획 전환: + - Phase 6 `Task 6.4` / `P6-R1` + - Phase 15 `Task 15.3` / `P15-R2` + - Phase 16 `Task 16.3` / `P16-R2` + - Phase 19 `Task 19.3` / `P19-R2` +- 검증: + - `MainHomeView.swift`, `CommunityPostCard.swift`, `I18n.swift`, PRD와 PBX Sources phase를 줄 단위로 대조해 각 실패 조건을 재현 + - PBX Sources phase 대조 결과 `V2ExpandableTextView.swift in Sources` 포함 수 `SodaLive=2`, `SodaLive-dev=1` + - `xcodebuild -workspace SodaLive.xcworkspace -scheme SodaLive -configuration Debug -derivedDataPath /private/tmp/SodaLiveDerivedDataReviewCurrentProd build` 실행, `BUILD SUCCEEDED` + - 필터링한 증분 빌드에서 `Skipping duplicate build file ... V2ExpandableTextView.swift` 경고 재현, 종료 코드 0 + - `SodaLive-dev` Debug 빌드는 같은 현재 source/project 상태에 대한 2026-07-31 Phase 22.3 검증에서 `BUILD SUCCEEDED` +- 범위: + - 리뷰 리포트와 후속 계획만 갱신했으며 신규 확정 항목의 production 코드·프로젝트·PRD 수정은 수행하지 않음 + - 기존 완료 체크와 과거 검증 기록은 유지하고 신규 Task는 미완료 상태로 추가 + +### 2026-07-31 Phase 22.3 모두 팔로우 확인 흐름 수동 검증 완료 + +- 목적: Confirm Dialog가 의도하지 않은 대량 팔로우를 차단하면서 기존 인증·완료·loading·오류 흐름을 유지하는지 최종 판정 +- 수동 확인 결과: + - 로그인 사용자가 `모두 팔로우하기`를 터치하면 Dialog가 표시되고 확인 전 API 요청은 발생하지 않음 + - `취소` 또는 dimmed 영역 터치 시 Dialog가 닫히고 API 요청은 발생하지 않음 + - `팔로우 하기`를 한 번 터치하면 Dialog가 닫히고 해당 섹션의 기존 API 요청이 1회 발생함 + - 비로그인 사용자는 Dialog 없이 로그인 화면으로 이동하고, 로그인 완료 후 자동 팔로우하지 않음 + - 기존 완료/호출 중 버튼 상태와 API 실패 toast에서 발견된 오류 없음 + - 앱 언어 ko/en/ja에서 제목, 설명, 확인, 취소 문구 표시에서 발견된 오류 없음 +- 검증: + - 사용자 수동 테스트 결과 발견된 오류 없음 + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -derivedDataPath "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataFinalCheck" build`: `BUILD SUCCEEDED` + - `Task 22.3` 완료 처리 + +### 2026-07-30 리뷰 보완 Task 2.3/5.2/12.2/15.2/16.2/19.2 처리 + +- 목적: 통합 리뷰에서 확정된 `REV-P2-001`, `REV-P5-001`, `REV-P12-001`, `REV-P15-001`, `REV-P16-001`, `REV-P19-001` 보완 +- 수행 내용: + - `HomeRecommendationResponse`에서 PRD 응답 계약에 없는 `firstAudioContents`와 `HomeFirstAudioContentItem` 제거 + - 추천 AI 캐릭터 탭 callback을 `creatorId`가 아니라 `characterId`로 전달하도록 수정 + - 추천 AI 캐릭터 전용 `handleRecommendationCharacterTap(characterId:)`를 추가하고 기존 추천 상세 guard를 거쳐 `.characterDetail(characterId:)`로 이동하도록 연결 + - `CommunityPostCard.isAccessiblePost`를 `price <= 0 || existOrdered`로 수정 + - 사업자 정보 섹션의 private `BusinessInfoExpandableTextView` 중복 구현을 제거하고 V2 공용 `V2ExpandableTextView`로 단일화 + - legacy `CustomView/ExpandableTextView`와 산출물 충돌을 피하기 위해 V2 공용 파일명을 `V2ExpandableTextView.swift`로 정리하고 Xcode Sources에 포함 + - PRD와 Phase 17 활성 기준에서 장르 크리에이터 섹션 미표시 결정을 반영하고, `genreCreators` 응답 모델 유지는 구분해 기록 +- TDD 예외/대체 검증: + - 저장소에 앱 테스트 번들 타깃이 없어 production Swift 모델/UI callback을 import하는 영구 unit test는 추가하지 않음 + - `swift -module-cache-path "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/sodalive-swift-module-cache" -e ''` 실행, `decoded without firstAudioContents` 확인 +- 검증: + - `rg -n "firstAudioContents|HomeFirstAudioContentItem" "SodaLive/Sources/V2/Main/Home/Recommendation"` 실행, 검색 결과 없음 확인 + - `rg -n "onTapCharacter:|handleRecommendationCharacterTap|characterDetail\(characterId:|onTapCharacter\(item\.(characterId|creatorId)\)" "SodaLive/Sources/V2/Main/MainView.swift" "SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeAiCharacterSection.swift"` 실행, `item.characterId`, `handleRecommendationCharacterTap`, `characterDetail(characterId:)` 연결 확인 + - `rg -n "price (==|<=) 0 \|\| existOrdered|isAccessiblePost|isLocked" "SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift"` 실행, `price <= 0 || existOrdered` 확인 + - `rg -n "V2ExpandableTextView|BusinessInfoExpandableTextView|struct ExpandableTextView|struct V2ExpandableTextView" "SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeBusinessInfoSection.swift" "SodaLive/Sources/V2/Component/Text" "SodaLive/Sources/CustomView/ExpandableTextView.swift"` 실행, 사업자 섹션은 V2 공용 구현을 사용하고 legacy 타입은 기존 경로에만 남음 확인 + - `rg -n "장르의 크리에이터|MainHomeGenreCreatorSection|genreCreators" "docs/20260602_메인_홈_추천_UI_API_연동" "SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift"` 실행, 최신 미표시 결정과 과거 기록 구분 확인 + - `plutil -lint "SodaLive.xcodeproj/project.pbxproj"` 실행, `OK` + - `git diff --check` 실행, 출력 없이 성공 + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -derivedDataPath "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataReviewSupplement" build` 실행, V2/legacy 파일명이 모두 `ExpandableTextView.swift`라 `Multiple commands produce ... ExpandableTextView.stringsdata`로 실패 확인 + - V2 공용 파일명을 `V2ExpandableTextView.swift`로 정리 후 `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -derivedDataPath "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataReviewSupplement2" build` 최초 실행은 10분 제한으로 `BUILD INTERRUPTED` 확인 + - 동일 빌드 명령 재실행, `BUILD SUCCEEDED` 확인 + - `lsp_diagnostics`는 개별 Swift 파일 단독 분석에서 기존 프로젝트 타입과 외부 모듈(`Kingfisher`, `Bootpay`)을 찾지 못해 false positive를 반환했으므로, 실제 컴파일 검증은 `xcodebuild` 결과를 기준으로 판단 +- 미실행 수동 확인: + - 실제 기기/시뮬레이터에서 AI 캐릭터 탭, 커뮤니티 가격 상태 UI, 사업자 정보 3줄 이하/초과 토글, 모두 팔로우 API 요청 0/1회, ko/en/ja 표시 확인은 아직 수행하지 못함 + - `Task 22.3`은 실제 로그인/네트워크/언어 화면 수동 확인이 필요하므로 미완료 유지 + +### 2026-07-30 Phase 1~22 구현 통합 리뷰 및 후속 Task 전환 + +- 목적: 완료 또는 진행된 각 Phase의 계획 체크, 실제 코드·프로젝트 포함 상태·검증 기록을 현재 PRD와 저장소 가이드에 대조하고, 확정된 수정 항목을 소유 Phase의 실행 가능한 신규 Task로 전환 +- 리뷰 결과: + - Phase별 리포트 22개를 `docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase<번호>-*.md`로 분리 작성 + - `REV-P2-001` High: PRD에 없는 non-optional `firstAudioContents` 때문에 기준 응답이 `keyNotFound`로 전체 디코딩 실패 가능 + - `REV-P5-001` High: 추천 AI 캐릭터 callback이 크리에이터 상세 handler에 연결됨 + - `REV-P12-001` High: AI 캐릭터 카드가 `characterId` 대신 `creatorId`를 callback으로 전달함 + - `REV-P15-001` Low: 커뮤니티 무료 판정이 PRD의 `price <= 0`이 아니라 `price == 0`으로 구현됨 + - `REV-P16-001` Low: 공용 V2 확장 텍스트 파일은 Sources 미포함이고 사업자 화면에 동일 구현이 복제됨 + - `REV-P19-001` Low: 장르 섹션 미표시 결정과 PRD/Phase 17 활성 기준이 충돌함 + - Phase 22는 정적/빌드 결함이 추가로 확인되지 않았으나 실제 요청 0/1회와 ko/en/ja 화면 확인이 미실행이므로 기존 Task 22.3을 미완료로 유지 +- 계획 전환: + - Phase 2 `Task 2.3` / `P2-R1` + - Phase 5 `Task 5.2` / `P5-R1` + - Phase 12 `Task 12.2` / `P12-R1` + - Phase 15 `Task 15.2` / `P15-R1` + - Phase 16 `Task 16.2` / `P16-R1` + - Phase 19 `Task 19.2` / `P19-R1` +- 검증: + - `git diff --check` 실행, 출력 없이 성공 + - `plutil -lint SodaLive.xcodeproj/project.pbxproj` 실행, `OK` + - 제외 섹션/Figma URL/기존 `HomeApi` 추천 endpoint 정적 검색, 검색 결과 없음 + - `swift -module-cache-path /private/tmp/sodalive-review-swift-module-cache -e '<필수 Decodable 키 누락 재현>'` 실행, `keyNotFound(firstAudioContents)` 재현 + - 샌드박스 내 `xcodebuild -workspace "SodaLive.xcworkspace" -list`는 CoreSimulator/캐시 접근 제한으로 실패 + - 샌드박스 밖 `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -derivedDataPath "/private/tmp/SodaLiveDerivedDataReview20260730" build` 재실행, `BUILD SUCCEEDED` +- 범위: + - 리뷰 리포트와 후속 계획만 작성했으며 production Swift 코드는 수정하지 않음 + - 기존 완료 체크와 과거 검증 기록은 삭제하거나 되돌리지 않음 + +### 2026-07-30 Phase 22 모두 팔로우 Confirm Dialog 코드 구현 + +- 목적: 로그인 사용자의 추천 탭 `모두 팔로우하기` API 호출을 Confirm Dialog 확인 이후로 지연 +- 수행 내용: + - `MainView.handleRecommendationFollowAllTap`에서 비로그인 사용자는 기존처럼 로그인 화면으로 이동하도록 유지 + - 로그인 사용자는 `SodaV2ActionModal` Confirm Dialog를 표시하고, `팔로우 하기` 확인 시 보류한 followAll action을 1회 실행하도록 연결 + - `취소`와 dimmed 영역 터치 시 Dialog를 닫고 보류 action을 폐기하도록 연결 + - Dialog 제목, 설명, 확인 버튼 문구를 `I18n.HomeRecommendation`에 ko/en/ja로 추가하고 취소 버튼은 `I18n.Common.cancel` 재사용 + - 기존 `MainHomeRecommendationViewModel.followAll`, `FollowRecommendedCreatorsRequest`, Repository/API 계약은 변경하지 않음 +- TDD 예외 사유: + - `SodaLive-dev` scheme의 test action이 구성되어 있지 않고, SwiftUI Dialog와 실제 네트워크 요청 횟수 통합 흐름을 위한 신규 테스트 인프라 추가는 이번 최소 변경 범위를 벗어남 +- 검증: + - `rg -n "handleRecommendationFollowAllTap|SodaV2ActionModal|pendingRecommendationFollowAllAction|isShowRecommendationFollowAllConfirmDialog|followAllConfirm|I18n.Common.cancel" "SodaLive/Sources/V2/Main/MainView.swift" "SodaLive/Sources/I18n/I18n.swift"` 실행, 로그인 guard 뒤 Confirm Dialog 표시, 보류 action, 확인/취소 처리와 다국어 키 확인 + - `rg -n "followRecommendedCreators|FollowRecommendedCreatorsRequest" "SodaLive/Sources/V2/Main/Home/Recommendation"` 실행, 기존 모두 팔로우 API와 request 경로 유지 확인 + - `git diff --check` 실행, 출력 없이 성공 확인 + - `plutil -lint "SodaLive.xcodeproj/project.pbxproj"` 실행, `OK` 확인 + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -derivedDataPath "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataPhase22" build` 최초 실행은 10분 제한으로 `BUILD INTERRUPTED` 확인 + - 동일 빌드 명령 재실행, `BUILD SUCCEEDED` 확인 + - `lsp_diagnostics`는 개별 Swift 파일 단독 분석에서 기존 프로젝트 모듈(`Bootpay`)을 찾지 못해 false positive를 반환했으므로, 실제 컴파일 검증은 `xcodebuild` 결과를 기준으로 판단 +- 미실행 수동 확인: + - 실제 기기/시뮬레이터에서 로그인/비로그인, 확인 전 API 0회, 취소/dimmed API 0회, 확인 API 1회, ko/en/ja 표시 확인은 아직 수행하지 못했으므로 Task 22.3 Phase Gate는 미완료로 유지 + +### 2026-07-30 Phase 22 모두 팔로우 확인 Dialog 요구사항 문서화 + +- 목적: 의도하지 않은 다중 크리에이터 팔로우를 막기 위한 Confirm Dialog 요구사항과 실행 계획을 인터뷰 결정 기준으로 확정 +- 수행 내용: + - 기존 메인 홈 추천 UI/API 연동 PRD에 문제, 목표, 사용자 흐름, 적용 범위, 로그인 순서, ko/en/ja 문구, 성공 기준과 Decision Log 추가 + - 메인 홈 추천 탭의 모든 공통 `모두 팔로우하기` 진입점에 적용하되 현재 미노출 장르 섹션을 다시 표시하지 않도록 범위 확정 + - 비로그인 사용자는 기존 로그인 이동을 유지하고, 로그인 사용자는 `팔로우 하기` 확인 시에만 기존 API를 호출하도록 확정 + - 취소와 dimmed 영역 터치는 보류 action 폐기 및 API 미호출로 확정 + - 실제 Swift/I18n 구현과 빌드·수동 API 검증은 Task 22.2~22.3 미완료 상태로 분리 +- 검증: + - `rg "모두 팔로우 확인 Dialog|모두 팔로우할까요|Follow all creators|すべてのクリエイターをフォローしますか|dimmed|자동 팔로우" docs/20260602_메인_홈_추천_UI_API_연동/prd.md docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` 실행, 적용 범위, 인증 순서, 취소 동작과 ko/en/ja 문구가 두 문서에서 검색됨 확인 + - `git diff --check -- docs/20260602_메인_홈_추천_UI_API_연동/prd.md docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` 실행, 출력 없이 성공 확인 + ### 2026-07-30 Phase 21 최근 활동 LIVE 이동 fallback 구현 완료 - 목적: 최근 활동 크리에이터의 `LIVE` fallback과 `LIVE_REPLAY` 콘텐츠 상세 이동을 코드에 반영 diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/prd.md b/docs/20260602_메인_홈_추천_UI_API_연동/prd.md index d8093c2f..71c54c4b 100644 --- a/docs/20260602_메인_홈_추천_UI_API_연동/prd.md +++ b/docs/20260602_메인_홈_추천_UI_API_연동/prd.md @@ -7,14 +7,15 @@ ## 2. Problem - 추천 탭 전용 데이터가 기존 홈 API와 섞이면 응답/화면 책임이 커지고 회귀 위험이 커진다. -- Figma 추천 탭은 라이브, 배너, 최근 활동, 최근 데뷔, AI 캐릭터, 장르 크리에이터, 응원 크리에이터, 인기 커뮤니티, 사업자 정보 등 여러 독립 섹션으로 구성된다. +- Figma 추천 탭은 라이브, 배너, 최근 활동, 최근 데뷔, AI 캐릭터, 응원 크리에이터, 인기 커뮤니티, 사업자 정보 등 여러 독립 섹션으로 구성된다. - 섹션별 UI가 많으므로 한 화면 파일에 직접 구현하면 유지보수와 검증이 어려워진다. - `FeedCommunityView`는 추천 탭 요구에 맞춰 키워드 영역 제거, 이미지 표시, 오디오 재생, 유료/구매 상태 UI가 필요하다. - 사업자 정보는 외부 라이브러리 없이 최대 3줄 말줄임표, 더보기, 접기 동작을 제공해야 한다. +- `모두 팔로우하기`는 한 번의 요청으로 여러 크리에이터를 팔로우하지만 현재 버튼 터치 즉시 API를 호출하므로, 사용자가 대상을 충분히 확인하지 못한 상태에서 의도하지 않은 크리에이터까지 팔로우할 수 있다. ## 3. Goals - `추천` 탭 진입 시 `GET /api/v2/home/recommendations`를 호출하고 응답 데이터로 화면을 구성한다. -- 기존 API에 추천 endpoint를 추가하지 않고 신규 `MainHomeApi`, Repository, ViewModel 계층을 만든다. +- 기존 API에 추천 endpoint를 추가하지 않고 신규 `MainHomeRecommendationApi`, Repository, ViewModel 계층을 만든다. - Figma 확인 시 이미 생성된 widget이나 기존 V2 공용 컴포넌트로 처리 가능한 부분은 재사용한다. - `MainHomeView`를 추천/랭킹/팔로잉 탭 shell로 유지하고, 추천 탭 콘텐츠는 `MainHomeRecommendationView`에서 조립한다. - 랭킹/팔로잉 탭은 `MainHomeRankingView`, `MainHomeFollowingView`로 별도 파일/폴더를 준비해 이후 구현이 MainHome shell에 누적되지 않게 한다. @@ -23,6 +24,7 @@ - 빈 데이터 섹션은 제목과 컨테이너를 표시하지 않는다. - 사업자 정보 섹션은 화면 width를 채우고, 기본 3줄 말줄임표와 더보기/접기 전환을 지원한다. - `FeedCommunityView`는 추천 탭 요구에 맞게 세로 피드형 이미지/오디오/유료 커뮤니티 포스트 UI를 지원한다. +- 추천 탭의 모든 `모두 팔로우하기` 진입점은 로그인 사용자에게 확인 Dialog를 먼저 표시하고, 명시적으로 확인한 경우에만 기존 모두 팔로우 API를 호출한다. ## 4. Non-Goals - `추천 필모그래피` 섹션은 만들지 않는다. @@ -32,6 +34,9 @@ - Figma 로컬 asset URL 또는 Figma 웹 URL을 앱 코드에 직접 사용하지 않는다. - 서버 응답 스펙에 없는 개별 팔로우 상태, 페이지네이션, 정렬/필터 기능은 이번 범위에 포함하지 않는다. - Figma `node-id=309-19775`의 `구매완료` 버튼은 구현하지 않는다. +- 장르 크리에이터 섹션은 현재 추천 탭에 노출하지 않는다. 단, API 디코딩을 위해 `genreCreators` 응답 모델 필드는 유지한다. +- 개별 크리에이터 팔로우/언팔로우 흐름에는 이번 Confirm Dialog를 적용하지 않는다. +- 기존 모두 팔로우 API의 endpoint, request/response 계약과 서버 동작은 변경하지 않는다. ## 5. Target Users - 앱 홈에서 추천 라이브, 크리에이터, 콘텐츠, 커뮤니티를 빠르게 탐색하는 사용자 @@ -44,6 +49,7 @@ - 사용자는 이미지가 포함된 커뮤니티 포스트와 유료/구매 상태를 카드에서 구분하고 싶다. - 사용자는 사업자 정보를 기본적으로 짧게 보고, 필요할 때 더보기/접기로 전환하고 싶다. - 사용자는 로그인이 필요하거나 민감 콘텐츠 제한이 필요한 상세 화면에 진입할 때 기존 홈과 같은 로그인/본인인증/콘텐츠 보기 설정 안내를 받고 싶다. +- 사용자는 여러 크리에이터를 한 번에 팔로우하기 전에 영향을 확인하고, 원하지 않으면 API 요청 없이 취소하고 싶다. ## 7. Core Requirements @@ -104,6 +110,7 @@ data class HomeGenreCreatorGroupItem( val genreName: String, val creators: List ) +``` @@ -124,6 +131,8 @@ data class RecommendationBannerResponse( - `seriesId` -> 시리즈 상세 - `link` -> 외부 URL 열기 +#### HomePopularCommunityPostItem +```kotlin data class HomePopularCommunityPostItem( val postId: Long, val creatorId: Long, @@ -230,7 +239,32 @@ enum class RecommendedActivityType(val code: String) { 구현 방향: - SwiftUI `Text`와 `lineLimit(isExpanded ? nil : 3)` 기반으로 구현한다. - 줄 수 판정은 제한/무제한 높이 비교 또는 Geometry 기반 측정 방식을 사용한다. -- 사업자 정보 wrapper는 `MainHomeBusinessInfoSection`, 재사용 가능한 텍스트 UI는 `ExpandableTextView`로 분리한다. +- 사업자 정보 wrapper는 `MainHomeBusinessInfoSection`, 재사용 가능한 텍스트 UI는 `V2ExpandableTextView`로 분리한다. + +### 7.6 모두 팔로우 확인 Dialog + +적용 범위: +- 메인 홈 추천 탭에서 공통 `onTapFollowAll` 흐름을 사용하는 모든 `모두 팔로우하기` 진입점에 적용한다. +- 현재 화면에 노출되는 `최근 응원이 많은 크리에이터` 섹션뿐 아니라, 같은 공통 흐름에 연결되는 다른 모두 팔로우 진입점도 동일한 확인 절차를 사용한다. +- 현재 미노출 상태인 장르 크리에이터 섹션을 다시 노출하는 작업은 이번 범위에 포함하지 않는다. + +로그인 및 확인 흐름: +1. 비로그인 사용자가 `모두 팔로우하기`를 터치하면 기존 동작과 동일하게 확인 Dialog 없이 로그인 화면으로 이동한다. +2. 로그인 사용자가 `모두 팔로우하기`를 터치하면 기존 `SodaV2ActionModal`을 사용한 Confirm Dialog를 표시한다. +3. Dialog가 표시된 시점에는 `POST /api/v2/home/recommendations/creators/follow`를 호출하지 않는다. +4. 사용자가 `팔로우 하기`를 터치하면 Dialog를 닫고 선택한 섹션의 기존 모두 팔로우 action을 한 번 실행한다. +5. 사용자가 `취소` 또는 dimmed 영역을 터치하면 Dialog를 닫고 보류한 action을 폐기하며 API를 호출하지 않는다. +6. 로그인 화면에서 돌아온 뒤 모두 팔로우를 자동 실행하지 않는다. 사용자가 버튼을 다시 터치하면 확인 Dialog부터 새로 진행한다. +7. 기존 모두 팔로우 완료, 호출 중, 오류 toast와 중복 호출 방지 동작은 유지한다. + +다국어 문구: + +| 용도 | ko | en | ja | +|---|---|---|---| +| 제목 | `모두 팔로우할까요?` | `Follow all creators?` | `すべてのクリエイターをフォローしますか?` | +| 설명 | `추천된 크리에이터를 모두 팔로우합니다.` | `You’ll follow all recommended creators.` | `おすすめのクリエイターをすべてフォローします。` | +| 확인 | `팔로우 하기` | `Follow` | `フォローする` | +| 취소 | `I18n.Common.cancel` 재사용 | `I18n.Common.cancel` 재사용 | `I18n.Common.cancel` 재사용 | ## 8. UX / UI Expectations @@ -256,7 +290,6 @@ MainHomeView │ ├─ 최근 활동 크리에이터 섹션 │ ├─ 최근 데뷔한 크리에이터 섹션 │ ├─ AI 캐릭터 섹션 - │ ├─ 장르의 크리에이터 섹션 │ ├─ 최근 응원이 많은 크리에이터 섹션 │ ├─ 인기 커뮤니티 섹션 │ └─ 사업자 정보 섹션 @@ -273,7 +306,7 @@ MainHomeView - `SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift` - `SodaLive/Sources/V2/Component/Creator/CreatorProfileGrid.swift` - `SodaLive/Sources/V2/Component/Creator/CreatorProfileItem.swift` -- `SodaLive/Sources/V2/Component/Text/ExpandableTextView.swift` +- `SodaLive/Sources/V2/Component/Text/V2ExpandableTextView.swift` 이미 존재하는 컴포넌트가 Figma 요구를 일부만 충족하면 새 컴포넌트를 중복 생성하지 않고 필요한 범위만 보완한다. @@ -296,6 +329,8 @@ MainHomeView - 인증 헤더는 기존 `UserDefaultsKey.token` 기반 패턴을 따른다. - 서버 `Long` 값은 Swift에서 기존 라우팅/모델 관례와 맞는 `Int` 또는 `Int64`로 사용한다. - 사용자 노출 문구는 가능한 `I18n`에 추가한다. +- 모두 팔로우 Confirm Dialog는 기존 `SodaV2ActionModal`과 `I18n.Common.cancel`을 재사용하고 새 Dialog 컴포넌트 또는 외부 의존성을 추가하지 않는다. +- 모두 팔로우 API/Repository/ViewModel 계약은 유지하고, 공통 callback에서 API action 실행 시점만 확인 이후로 지연한다. - 상세 진입 guard는 `MainHomeView` 내부에 인증 UI를 중복 구현하지 않고, V2 `MainView`의 기존 인증/민감 콘텐츠 guard 인프라를 재사용한다. - `MainHomeView`에는 추천 탭의 섹션 조립을 직접 누적하지 않고, 탭별 콘텐츠 View를 조합하는 역할만 둔다. - Open Questions에 남긴 항목은 임의 구현하지 않고 확인 후 반영한다. @@ -315,6 +350,11 @@ MainHomeView - 최근 활동 크리에이터의 `COMMUNITY` 활동과 인기 커뮤니티 섹션의 게시물은 커뮤니티 게시물 상세 페이지로 이동한다. - `MainHomeView`는 추천/랭킹/팔로잉 탭 shell 역할만 하고, 추천 탭 섹션 조립은 `MainHomeRecommendationView`에 분리되어 있다. - 반복 UI는 기존 widget 또는 공용 컴포넌트를 재사용하고 불필요하게 중복 구현하지 않는다. +- 로그인 사용자가 추천 탭의 `모두 팔로우하기`를 터치하면 Confirm Dialog가 표시되고, 확인 전에는 모두 팔로우 API 요청이 발생하지 않는다. +- Dialog에서 `팔로우 하기`를 한 번 터치하면 해당 섹션의 기존 모두 팔로우 API가 한 번 호출된다. +- Dialog에서 `취소` 또는 dimmed 영역을 터치하면 모두 팔로우 API 요청이 발생하지 않는다. +- 비로그인 사용자는 기존처럼 로그인 화면으로 이동하며, 확인 Dialog 또는 로그인 완료 후 자동 팔로우가 실행되지 않는다. +- 모두 팔로우 Dialog의 제목, 설명, 확인 버튼은 ko/en/ja로 표시되고 취소 버튼은 기존 공통 I18n을 재사용한다. - 빌드가 성공하고, 가능하면 ViewModel 디코딩/상태 전환 검증을 수행한다. ## 11. Open Questions @@ -323,9 +363,22 @@ MainHomeView - `HomeLiveItem`에 민감/성인 라이브 여부를 판단할 `isAdult` 또는 동등한 필드가 추가되는지, 아니면 서버에서 추천 라이브를 사전 필터링하는지 확인이 필요하다. - 커뮤니티 `audioUrl`을 추천 탭 카드에서 표시/재생해야 하는지 확인이 필요하다. - 각 카드 터치 시 상세 이동 대상이 모두 정의되어 있는지 확인이 필요하다. 단, 최근 활동 `LIVE`, `LIVE_REPLAY`, `COMMUNITY`, 인기 커뮤니티 게시물의 이동 규칙은 확정한다. +- 모두 팔로우 확인 Dialog의 적용 범위, 로그인 순서, 취소 동작, 문구와 다국어에는 열린 질문이 없다. ## 12. Verification Plan - 문서 검증: API URL, 응답 모델, 제외 섹션, FeedCommunityView, 사업자 정보 요구가 PRD와 계획 문서에 반영되었는지 확인한다. - 정적 검증: 신규 추천 API가 기존 `HomeApi`에 추가되지 않았고, Figma URL이 앱 코드에 직접 포함되지 않았는지 확인한다. - 빌드 검증: `docs/agent-guides/build-test-verification.md` 기준으로 가능한 iOS 빌드 또는 최소 정적 검증을 실행한다. - 기능 검증: 추천 API 성공/실패, 빈 섹션, 활동 타입 I18n, 상세 진입 guard, 최근 활동 `LIVE`의 `targetId` 유무별 이동, `creatorId` 양수 검사, `LIVE_REPLAY`의 콘텐츠 상세 이동, 다른 활동 타입의 회귀 여부, 커뮤니티 카드 상태, 사업자 정보 더보기/접기를 확인한다. +- 모두 팔로우 기능 검증: 로그인/비로그인 상태를 구분해 Dialog 표시 여부를 확인하고, 확인 전·취소·dimmed 영역 터치에는 API 요청이 0회이며 `팔로우 하기` 선택 시에만 요청이 1회인지 확인한다. +- 다국어 검증: 앱 언어를 ko/en/ja로 전환해 모두 팔로우 Dialog의 제목, 설명, 확인, 취소 문구를 확인한다. + +## 13. Decision Log + +### 2026-07-30 모두 팔로우 확인 Dialog 인터뷰 + +- 메인 홈 추천 탭의 모든 `모두 팔로우하기` 진입점에 공통 확인 Dialog를 적용한다. +- 비로그인 사용자는 기존처럼 로그인 화면으로 바로 이동하고, 로그인 후 자동 팔로우하지 않는다. +- 로그인 사용자는 `팔로우 하기`를 명시적으로 선택한 경우에만 API를 호출한다. +- `취소`와 dimmed 영역 터치는 모두 보류 action 폐기 및 API 미호출로 처리한다. +- Dialog 문구는 7.6의 ko/en/ja 문구로 확정하며 관련 열린 질문은 없다. diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase1-spec-and-existing-implementation.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase1-spec-and-existing-implementation.md new file mode 100644 index 00000000..40d6d84a --- /dev/null +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase1-spec-and-existing-implementation.md @@ -0,0 +1,64 @@ +# Phase 1 기준 스펙과 기존 구현 점검 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 1 / Task 1.1~1.2 | +| 기준 commit 또는 working tree | `3ae2dc19d67d0a88bc5043f970e4cfabdfb28b60` + 현재 working tree | +| 리뷰 일자 | 2026-07-30 | +| 리뷰어 | Codex | +| 기준 문서 | `prd.md`, `plan-task.md`, 저장소 가이드 | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +- PRD 기준점, V2 홈/공용 컴포넌트 inventory와 제외 범위가 실제 저장소 상태와 일치하는지 확인한다. +- `SodaLive/Sources/V2/Main/Home/**`, `SodaLive/Sources/V2/Component/**`를 포함하고 이후 Phase의 기능 판정은 제외한다. + +## 3. 판정 기준 + +- `docs/sample/sample-review.md`의 Blocker/High/Medium/Low 및 후보/확정/오탐/보류 상태를 적용했다. +- 완료 체크와 실제 파일 존재, 금지 문자열 부재, 후속 요구사항 추적 가능성을 기준으로 판정했다. + +## 4. 검토한 근거와 실행한 검증 + +- 코드: `SodaLive/Sources/V2/Main/Home/**`, `SodaLive/Sources/V2/Component/**` +- 문서: PRD 3~4, 계획의 재분류 원칙과 Task 1.1~1.2 +- 환경: macOS 26.0, Xcode 26.0, Swift 6.2 + +| 명령 | 결과 | 핵심 증거 | +|---|---|---| +| `find SodaLive/Sources/V2/Main/Home SodaLive/Sources/V2/Component -type f -name '*.swift'` | 성공 | 홈/공용 컴포넌트 inventory 확인 | +| `rg "추천 필모그래피\|또 다른 모습\|figma.com\|localhost:3845" SodaLive/Sources/V2/Main/Home SodaLive/Sources/V2/Component` | 성공 | 검색 결과 없음 | +| `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -derivedDataPath "/private/tmp/SodaLiveDerivedDataReview20260730" build` | 성공 | `BUILD SUCCEEDED` | + +## 5. 발견 사항 요약 + +**확정 발견 사항 없음.** + +## 6. 발견 사항 상세 + +확정 발견 사항 없음. 이후 Phase에서 확인된 계약·라우팅 문제는 해당 Phase 리포트가 소유한다. + +## 7. 확정 항목의 plan·goal 전환 + +전환 항목 없음. + +## 8. 리뷰 종료 판정 + +| 판정 항목 | 결과 | 근거 | +|---|---|---| +| 리뷰 범위 전체 확인 | 충족 | inventory와 제외 문자열 확인 | +| 후보 항목 판정 완료 | 충족 | 남은 후보 없음 | +| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 | +| 검증 명령과 결과 기록 | 충족 | 4절 기록 | + +**최종 결론:** 확정 발견 사항 없음 + +## 9. 수정 후 검증 기록 + +### 2차 통합 재점검 — 2026-07-31 + +- PRD, 계획, V2 홈·공용 컴포넌트 목록과 제외 범위를 다시 대조했다. +- Phase 1 범위에서 추가 확정 발견 사항 없음. 후속 구현 결함은 각 소유 Phase 리포트로 분리했다. diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase10-active-creators.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase10-active-creators.md new file mode 100644 index 00000000..dd9a6a7a --- /dev/null +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase10-active-creators.md @@ -0,0 +1,88 @@ +# Phase 10 최근 활동 크리에이터 섹션 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 10 / Task 10.1 | +| 기준 commit 또는 working tree | `3ae2dc19d67d0a88bc5043f970e4cfabdfb28b60` + 현재 working tree | +| 리뷰 일자 | 2026-07-30 | +| 리뷰어 | Codex | +| 기준 문서 | PRD 7.1~7.3, `plan-task.md` Phase 10 | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +- 활동 크리에이터 표시 필드, 활동 문구, 상대 시간, 빈 상태 숨김을 확인한다. +- 탭 목적지 세부 회귀는 Phase 21 리포트가 소유한다. + +## 3. 판정 기준 + +- 응답 필드 소비, unknown 문구 숨김, 빈 배열 미표시와 컴파일 성공을 기준으로 판정한다. + +## 4. 검토한 근거와 실행한 검증 + +- 코드: `MainHomeActiveCreatorSection.swift:3-137`, `RecommendedActivityType.swift` +- 환경: macOS 26.0, Xcode 26.0, Swift 6.2 + +| 명령 | 결과 | 핵심 증거 | +|---|---|---| +| `rg "activityType\|activityAt\|targetId\|RecommendedActivityType\|relativeActivityAtText" ...` | 성공 | 표시/시간/타입 연결 확인 | +| `xcodebuild ... -derivedDataPath "/private/tmp/SodaLiveDerivedDataReview20260730" build` | 성공 | `BUILD SUCCEEDED` | + +## 5. 발견 사항 요약 + +**확정 발견 사항 없음.** + +## 6. 발견 사항 상세 + +확정 발견 사항 없음. LIVE/LIVE_REPLAY 이동은 Phase 21 기존 리뷰에서도 확정 발견 사항 없음으로 판정됐다. + +## 7. 확정 항목의 plan·goal 전환 + +전환 항목 없음. + +## 8. 리뷰 종료 판정 + +| 항목 | 결과 | 근거 | +|---|---|---| +| 범위 확인 | 충족 | 표시/시간/empty 확인 | +| 후보 판정 | 충족 | 남은 후보 없음 | +| plan 반영 | 해당 없음 | 확정 발견 사항 없음 | +| 검증 기록 | 충족 | 4절 | + +**최종 결론:** 확정 발견 사항 없음 + +## 9. 수정 후 검증 기록 + +### 2차 통합 재점검 — 2026-07-31 + +- 최근 활동 타입별 callback과 `targetId`·creator fallback 분기를 다시 확인했다. +- 추가 확정 발견 사항 없음. LIVE fallback 상세는 Phase 21 리포트가 소유한다. + +### 3차 최종 재점검 — 2026-07-31 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | +|---|---|---|---|---| +| `REV-P10-001` | Low | 확정 | 최근 활동 상대 시간 계산이 공용 `DateParser`와 중복됨 | `Task 10.2` / `P10-R1` | + +#### REV-P10-001 — 공용 상대 시간 계산 미재사용 + +- **관찰 내용:** `MainHomeActiveCreatorSection.swift`의 `relativeActivityAtText(now:)`는 `DateParser.relativeTimeText(fromUTC:fallback:now:)`와 같은 파싱, 연·월·일·시·분 분기와 I18n 반환을 별도로 구현한다. +- **영향:** 상대 시간 정책을 수정할 때 공용 경로와 추천 홈 경로가 서로 달라질 수 있고 동일 로직을 두 곳에서 유지해야 한다. +- **근거:** `MainHomeActiveCreatorSection.swift:106-137`, `DateParser.swift:19-49`의 분기와 반환값 대조. +- **재현 또는 검증:** 두 함수의 `DateParser.parse`, `Calendar.dateComponents`, `timeIntervalSince`, `I18n.Time` 사용을 줄 단위로 비교하면 동일한 계산임을 확인할 수 있다. +- **권장 조치:** 최근 활동 표시가 공용 `DateParser.relativeTimeText`를 직접 재사용하게 하고 이 섹션의 중복 구현만 제거한다. +- **계획 전환:** `plan-task.md` Phase 10에 `Task 10.2` / `P10-R1`을 추가했다. + +**당시 결론:** `REV-P10-001` 수정 goal 필요 + +### REV-P10-001 수정 기록 — 2026-07-31 + +- `MainHomeActiveCreatorSection`의 최근 활동 상대 시간 표시를 `DateParser.relativeTimeText(fromUTC:fallback:)` 호출로 교체했다. +- 섹션 내부의 중복 `relativeActivityAtText(now:)` helper를 제거했다. +- `rg -n "dateComponents|timeIntervalSince|relativeTimeText|relativeActivityAtText" "SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeActiveCreatorSection.swift" "SodaLive/Sources/Common/DateParser.swift"` 실행 결과, 추천 섹션에는 공용 `DateParser.relativeTimeText` 호출만 남고 중복 helper는 검색되지 않았다. +- `git diff --check` — 성공, 출력 없음. +- `xcodebuild -quiet -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -destination "generic/platform=iOS" -derivedDataPath "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataTask184DevRetry" build` — 종료 코드 0. + +**현재 결론:** `REV-P10-001` 수정 완료 diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase11-recent-debut-creators.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase11-recent-debut-creators.md new file mode 100644 index 00000000..10c53ecf --- /dev/null +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase11-recent-debut-creators.md @@ -0,0 +1,82 @@ +# Phase 11 최근 데뷔 크리에이터 섹션 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 11 / Task 11.1 | +| 기준 commit 또는 working tree | `3ae2dc19d67d0a88bc5043f970e4cfabdfb28b60` + 현재 working tree | +| 리뷰 일자 | 2026-07-30 | +| 리뷰어 | Codex | +| 기준 문서 | `plan-task.md` Phase 11 | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +- `HomeCreatorItem` 표시, 빈 상태 숨김, creator callback과 현재 반응형 카드 구현을 확인한다. + +## 3. 판정 기준 + +- 필드와 callback ID 일치, 빈 배열 미표시, 기존 카드 크기 기준과 빌드 성공을 기준으로 판정한다. + +## 4. 검토한 근거와 실행한 검증 + +- 코드: `MainHomeRecentDebutCreatorSection.swift:3-71`, `AiCharacterCard.responsiveSize` +- 환경: macOS 26.0, Xcode 26.0, Swift 6.2 + +| 명령 | 결과 | 핵심 증거 | +|---|---|---| +| `rg "creatorId\|creatorNickname\|creatorProfileImage\|responsiveSize" MainHomeRecentDebutCreatorSection.swift` | 성공 | 표시와 creator ID 전달 확인 | +| `xcodebuild ... -derivedDataPath "/private/tmp/SodaLiveDerivedDataReview20260730" build` | 성공 | `BUILD SUCCEEDED` | + +## 5. 발견 사항 요약 + +**확정 발견 사항 없음.** + +## 6. 발견 사항 상세 + +확정 발견 사항 없음. `CreatorProfileGrid` 대신 직사각형 Figma 카드 전용 View를 사용한 것은 기존 공용 원형 프로필 UI로 충족할 수 없어 확정 결함으로 보지 않았다. + +## 7. 확정 항목의 plan·goal 전환 + +전환 항목 없음. + +## 8. 리뷰 종료 판정 + +| 항목 | 결과 | 근거 | +|---|---|---| +| 범위 확인 | 충족 | 표시와 callback 추적 | +| 후보 판정 | 충족 | 남은 후보 없음 | +| plan 반영 | 해당 없음 | 확정 발견 사항 없음 | +| 검증 기록 | 충족 | 4절 | + +**최종 결론:** 확정 발견 사항 없음 + +## 9. 수정 후 검증 기록 + +### 2차 통합 재점검 — 2026-07-31 + +- 최근 데뷔 데이터 매핑과 공용 `CreatorProfileGrid` 재사용, 빈 상태를 다시 확인했다. +- 추가 확정 발견 사항 없음. + +### 3차 통합 재점검 — 2026-07-31 + +- 2차 기록의 `CreatorProfileGrid` 재사용 문구는 실제 구현과 다른 오기다. 현재 `MainHomeRecentDebutCreatorSection`은 Figma 직사각형 카드 요구를 위해 전용 `MainHomeRecentDebutCreatorItemView`를 사용하며, 이 선택 자체는 6절 판정과 같이 코드 결함으로 보지 않는다. +- append-only 리뷰 정정은 Phase 19 `REV-P19-004` / `Task 19.4`로 전환했다. + +### 4차 통합 재점검 — 2026-07-31 + +- `REV-P19-004` 보완으로 2차 기록의 `CreatorProfileGrid` 재사용 오기는 위 3차 기록에서 append-only로 정정 완료했다. +- 현재 활성 결론은 `MainHomeRecentDebutCreatorSection`이 전용 `MainHomeRecentDebutCreatorItemView`를 사용하는 것이며, 추가 확정 발견 사항 없음 판정은 유지한다. + +### 5차 최종 재점검 — 2026-07-31 + +- production 코드는 최신 리뷰 결론대로 전용 `MainHomeRecentDebutCreatorItemView`를 사용하고 creator ID callback·빈 상태 처리를 유지하므로 추가 코드 결함은 없다. +- 다만 활성 `plan-task.md`의 `Task 11.1` 작업·기대 결과는 여전히 공용 원형 `CreatorProfileGrid` 재사용을 완료 조건으로 요구해 현재 코드 및 이 리포트의 결론과 충돌한다. +- 문서 정합성 결함은 Phase 19 `REV-P19-005` / `Task 19.5`가 소유하며, Phase 11 production 코드의 결론은 **확정 발견 사항 없음**을 유지한다. + +### 6차 최종 재점검 — 2026-07-31 + +- Phase 19 `REV-P19-005` 보완으로 활성 `Task 11.1`은 공용 원형 `CreatorProfileGrid` 우선 검토 원칙을 유지하되, Figma 직사각형 카드 형태를 충족하지 못할 때 전용 `MainHomeRecentDebutCreatorItemView`를 사용하는 현재 결정을 명시했다. +- 기존 2차~5차 기록은 삭제하지 않고 append-only 정정으로 보존했다. +- 현재 Phase 11 결론은 전용 카드 구현 유지와 **확정 발견 사항 없음**이다. diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase12-ai-character-section.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase12-ai-character-section.md new file mode 100644 index 00000000..8d670f88 --- /dev/null +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase12-ai-character-section.md @@ -0,0 +1,91 @@ +# Phase 12 AI 캐릭터 섹션 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 12 / Task 12.1 | +| 기준 commit 또는 working tree | `3ae2dc19d67d0a88bc5043f970e4cfabdfb28b60` + 현재 working tree | +| 리뷰 일자 | 2026-07-30 | +| 리뷰어 | Codex | +| 기준 문서 | PRD 7.1·7.3, `plan-task.md` Phase 12 | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +- AI 캐릭터 카드 필드 표시, 빈 상태, callback 식별자가 응답 계약과 상세 route에 맞는지 확인한다. +- 최종 `MainView` route는 `REV-P5-001`과 연계한다. + +## 3. 판정 기준 + +- `characterId`와 `creatorId`는 서로 다른 계약 식별자이므로 캐릭터 상세 callback에 creator ID를 전달하면 High로 판정한다. + +## 4. 검토한 근거와 실행한 검증 + +- 코드: `MainHomeAiCharacterSection.swift:3-40`, Preview `:44-68`, `HomeAiCharacterItem` +- 정상 route: `AppStep.characterDetail(characterId:)` +- 환경: macOS 26.0, Xcode 26.0, Swift 6.2 + +| 명령 | 결과 | 핵심 증거 | +|---|---|---| +| `rg "onTapCharacter\\(item\\.creatorId\\)\|characterId" MainHomeAiCharacterSection.swift` | 실패 발견 | callback은 creator ID, Preview는 두 ID를 구분 | +| `rg "characterDetail\\(characterId:" SodaLive/Sources` | 성공 | 앱의 캐릭터 상세 계약 확인 | +| `xcodebuild ... -derivedDataPath "/private/tmp/SodaLiveDerivedDataReview20260730" build` | 성공 | 같은 `Int`라 타입 검사가 오류를 검출하지 못함 | + +## 5. 발견 사항 요약 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | +|---|---|---|---|---| +| `REV-P12-001` | High | 확정 | AI 캐릭터 카드가 `characterId` 대신 `creatorId`를 전달함 | `Task 12.2` / `P12-R1` | + +## 6. 발견 사항 상세 + +### REV-P12-001 — AI 캐릭터 callback 식별자 오류 + +- **심각도:** High +- **상태:** 확정 +- **소유 Task:** `Task 12.2` + +**관찰 내용** + +카드 탭은 `onTapCharacter(item.creatorId)`를 호출한다. 같은 파일 Preview도 `characterId: 1`, `creatorId: 10`처럼 두 값을 명시적으로 구분하므로 현재 callback 값은 캐릭터 상세 식별자가 아니다. + +**영향** + +`REV-P5-001`의 route만 수정해도 creator ID를 character ID로 오인해 잘못된 캐릭터를 조회하거나 상세 조회가 실패한다. + +**권장 조치** + +최소 수정으로 `item.characterId`를 전달하고 `creatorId != characterId` fixture로 callback과 최종 상세를 검증한다. + +**판정 기록** + +- 2026-07-30 — 응답 모델, Preview fixture, callback과 AppStep 계약 대조로 확정. + +## 7. 확정 항목의 plan·goal 전환 + +`plan-task.md` Phase 12에 `Task 12.2` / `P12-R1`을 추가했다. + +## 8. 리뷰 종료 판정 + +| 항목 | 결과 | 근거 | +|---|---|---| +| 범위 확인 | 충족 | model→section callback 추적 | +| 후보 판정 | 충족 | `REV-P12-001` 확정 | +| plan 반영 | 충족 | Task 12.2 | +| 검증 기록 | 충족 | 4절 | + +**최종 결론:** 수정 goal 필요 + +## 9. 수정 후 검증 기록 + +- 2026-07-30 — `MainHomeAiCharacterSection`의 카드 탭 callback을 `onTapCharacter(item.creatorId)`에서 `onTapCharacter(item.characterId)`로 수정. +- `rg -n "onTapCharacter\(item\.(characterId|creatorId)\)" "SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeAiCharacterSection.swift"` 실행, `item.characterId` 전달 확인. +- `MainView.handleRecommendationCharacterTap(characterId:)`와 함께 `characterDetail(characterId:)` 경로 정적 연결 확인. +- `git diff --check`, `plutil -lint "SodaLive.xcodeproj/project.pbxproj"`, `SodaLive-dev` Debug 빌드 통과. +- 실제 기기/시뮬레이터 탭 수동 확인은 미실행이며 잔여 확인 사항으로 남김. + +### 2차 통합 재점검 — 2026-07-31 + +- 카드가 `item.characterId`를 callback으로 전달하고 Phase 5의 character route로 연결되는 현재 구현을 확인했다. +- `REV-P12-001`은 **수정 완료**로 판정했으며, 추가 확정 발견 사항 없음. diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase13-genre-creators.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase13-genre-creators.md new file mode 100644 index 00000000..5546195b --- /dev/null +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase13-genre-creators.md @@ -0,0 +1,63 @@ +# Phase 13 장르 크리에이터 섹션 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 13 / Task 13.1 및 2026-07-12 미표시 결정 | +| 기준 commit 또는 working tree | `3ae2dc19d67d0a88bc5043f970e4cfabdfb28b60` + 현재 working tree | +| 리뷰 일자 | 2026-07-30 | +| 리뷰어 | Codex | +| 기준 문서 | `plan-task.md` Phase 13과 재분류 원칙 | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +- 기존 장르 그룹 컴포넌트 구현과 후속 미표시 결정의 코드 반영을 확인한다. +- 장르 섹션을 다시 노출하는 작업은 제외한다. + +## 3. 판정 기준 + +- 모델 필드는 유지하되 `MainHomeRecommendationView`에서 컴포넌트를 조립하지 않는 최신 결정을 우선한다. + +## 4. 검토한 근거와 실행한 검증 + +- 코드: `MainHomeGenreCreatorSection.swift`, `MainHomeRecommendationResponse.swift:10`, 추천 루트 +- 이력: commit `47fe0e7`의 장르 섹션 조립 제거 +- 환경: macOS 26.0, Xcode 26.0, Swift 6.2 + +| 명령 | 결과 | 핵심 증거 | +|---|---|---| +| `rg "MainHomeGenreCreatorSection\|genreCreators" MainHomeRecommendationView.swift` | 성공 | 검색 결과 없음 | +| `rg "genreCreators" MainHomeRecommendationResponse.swift` | 성공 | 디코딩 모델 유지 | +| `xcodebuild ... -derivedDataPath "/private/tmp/SodaLiveDerivedDataReview20260730" build` | 성공 | 미노출 컴포넌트 포함 상태 빌드 성공 | + +## 5. 발견 사항 요약 + +**확정 발견 사항 없음.** + +## 6. 발견 사항 상세 + +코드는 최신 미표시 결정을 충족한다. PRD/Phase 17에 남은 과거 노출 기준은 `REV-P19-001`이 소유한다. + +## 7. 확정 항목의 plan·goal 전환 + +이 Phase의 전환 항목 없음. + +## 8. 리뷰 종료 판정 + +| 항목 | 결과 | 근거 | +|---|---|---| +| 최신 동작 확인 | 충족 | 루트 참조 없음, 모델 유지 | +| 후보 판정 | 충족 | 코드 결함 없음 | +| plan 반영 | 해당 없음 | 문서 결함은 Phase 19 소유 | +| 검증 기록 | 충족 | 4절 | + +**최종 결론:** 확정 발견 사항 없음 + +## 9. 수정 후 검증 기록 + +### 2차 통합 재점검 — 2026-07-31 + +- `genreCreators`는 응답 디코딩용으로 유지되고 활성 추천 화면에는 조립되지 않는 최신 결정을 다시 확인했다. +- 추가 확정 발견 사항 없음. diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase14-cheer-creators.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase14-cheer-creators.md new file mode 100644 index 00000000..4a88079f --- /dev/null +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase14-cheer-creators.md @@ -0,0 +1,61 @@ +# Phase 14 응원 크리에이터 섹션 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 14 / Task 14.1 | +| 기준 commit 또는 working tree | `3ae2dc19d67d0a88bc5043f970e4cfabdfb28b60` + 현재 working tree | +| 리뷰 일자 | 2026-07-30 | +| 리뷰어 | Codex | +| 기준 문서 | `plan-task.md` Phase 14, PRD 7.6 | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +- 응원 creator field, 공용 그룹 UI, creator 탭, 모두 팔로우 callback과 상태 연결을 확인한다. +- Confirm Dialog 동작은 Phase 22로 제외한다. + +## 3. 판정 기준 + +- 빈 배열 미표시, 표시 ID 집합의 follow action 연결, 완료/로딩 중복 방지를 기준으로 판정한다. + +## 4. 검토한 근거와 실행한 검증 + +- 코드: `MainHomeCheerCreatorSection.swift`, `MainHomeCreatorGroupSection`, `MainHomeRecommendationView.swift:56-69` +- 환경: macOS 26.0, Xcode 26.0, Swift 6.2 + +| 명령 | 결과 | 핵심 증거 | +|---|---|---| +| `rg "cheerCreators\|MainHomeCreatorGroupSection\|followAll\|completionKey" ...` | 성공 | 데이터와 상태/action 연결 | +| `xcodebuild ... -derivedDataPath "/private/tmp/SodaLiveDerivedDataReview20260730" build` | 성공 | `BUILD SUCCEEDED` | + +## 5. 발견 사항 요약 + +**확정 발견 사항 없음.** + +## 6. 발견 사항 상세 + +확정 발견 사항 없음. 확인 전 API 지연과 취소 동작은 Phase 22 리포트에서 별도 판정한다. + +## 7. 확정 항목의 plan·goal 전환 + +전환 항목 없음. + +## 8. 리뷰 종료 판정 + +| 항목 | 결과 | 근거 | +|---|---|---| +| 범위 확인 | 충족 | section→ViewModel action 추적 | +| 후보 판정 | 충족 | 남은 후보 없음 | +| plan 반영 | 해당 없음 | 확정 발견 사항 없음 | +| 검증 기록 | 충족 | 4절 | + +**최종 결론:** 확정 발견 사항 없음 + +## 9. 수정 후 검증 기록 + +### 2차 통합 재점검 — 2026-07-31 + +- 응원 크리에이터 그룹 UI와 공통 모두 팔로우 callback 연결을 다시 확인했다. +- 추가 확정 발견 사항 없음. 확인 Dialog의 최종 판정은 Phase 22 리포트가 소유한다. diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase15-popular-community-feed.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase15-popular-community-feed.md new file mode 100644 index 00000000..24a4c073 --- /dev/null +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase15-popular-community-feed.md @@ -0,0 +1,121 @@ +# Phase 15 인기 커뮤니티 Feed 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 15 / Task 15.1 | +| 기준 commit 또는 working tree | `3ae2dc19d67d0a88bc5043f970e4cfabdfb28b60` + 현재 working tree | +| 리뷰 일자 | 2026-07-30 | +| 리뷰어 | Codex | +| 기준 문서 | PRD 7.4, `plan-task.md` Phase 15 | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +- 세로 피드, 이미지/오디오/유료 상태, 구매/좋아요/상세 action, 반응 순서와 상대 시간을 확인한다. +- 실제 네트워크 성공 응답과 오디오 청취 품질은 제외한다. + +## 3. 판정 기준 + +- PRD의 조건식을 코드 분기와 직접 대조한다. 제한된 가격 경계 불일치는 Low로 판정한다. + +## 4. 검토한 근거와 실행한 검증 + +- 코드: `MainHomePopularCommunitySection.swift`, `CommunityPostCard.swift:65-243`, `MainHomeRecommendationViewModel.swift:112-182` +- 문서: PRD 7.4의 `price <= 0 || existOrdered` +- 환경: macOS 26.0, Xcode 26.0, Swift 6.2 + +| 명령 | 결과 | 핵심 증거 | +|---|---|---| +| `rg "ScrollView\\(\\.horizontal\|LazyHStack\|frame\\(width: 240\\)" ...` | 성공 | 검색 결과 없음, 세로 피드 유지 | +| `rg "price <= 0\|price == 0\|isAccessiblePost" ...` | 실패 발견 | PRD는 `<= 0`, 코드는 `== 0` | +| `rg "onTapPost\|onTapComment\|purchaseCommunityPost\|communityPostLike\|toggleContent" ...` | 성공 | 상세/구매/좋아요/오디오 연결 | +| `xcodebuild ... -derivedDataPath "/private/tmp/SodaLiveDerivedDataReview20260730" build` | 성공 | `BUILD SUCCEEDED` | + +## 5. 발견 사항 요약 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | +|---|---|---|---|---| +| `REV-P15-001` | Low | 확정 | 무료 판정이 `price <= 0`이 아니라 `price == 0`임 | `Task 15.2` / `P15-R1` | + +## 6. 발견 사항 상세 + +### REV-P15-001 — 무료 가격 경계 불일치 + +- **심각도:** Low +- **상태:** 확정 +- **소유 Task:** `Task 15.2` + +**관찰 내용** + +이미지 잠금은 `price > 0 && !existOrdered`로 올바르게 판단하지만 reaction bar 노출의 `isAccessiblePost`는 `price == 0 || existOrdered`다. 따라서 PRD가 무료로 정의한 음수 가격은 일반 이미지를 표시하면서 댓글/좋아요를 숨기는 서로 다른 상태가 된다. + +**근거** + +- 코드: `CommunityPostCard.swift:230-236` +- 문서: PRD 7.4, Task 15.1의 `price <= 0 || existOrdered` + +**재현** + +1. `imageUrl != nil`, `price == -1`, `existOrdered == false`인 카드를 구성한다. +2. `isLocked == false`지만 `isAccessiblePost == false`가 된다. +3. 요구 결과는 무료/접근 가능 상태다. + +**권장 조치** + +`isAccessiblePost`만 `price <= 0 || existOrdered`로 맞추고 음수·0·양수/구매 조합을 확인한다. + +**판정 기록** + +- 2026-07-30 — PRD 조건식과 private 계산 프로퍼티 대조로 확정. + +## 7. 확정 항목의 plan·goal 전환 + +`plan-task.md` Phase 15에 `Task 15.2` / `P15-R1`을 추가했다. + +## 8. 리뷰 종료 판정 + +| 항목 | 결과 | 근거 | +|---|---|---| +| 범위 확인 | 충족 | card/viewModel/action 추적 | +| 후보 판정 | 충족 | `REV-P15-001` 확정 | +| plan 반영 | 충족 | Task 15.2 | +| 검증 기록 | 충족 | 4절 | + +**최종 결론:** 수정 goal 필요 + +## 9. 수정 후 검증 기록 + +- 2026-07-30 — `CommunityPostCard.isAccessiblePost`를 `price == 0 || existOrdered`에서 `price <= 0 || existOrdered`로 수정. +- `isLocked`는 기존 `price > 0 && !existOrdered` 조건을 유지해 유료 미구매 잠금 조건은 변경하지 않음. +- `rg -n "price (==|<=) 0 \|\| existOrdered|isAccessiblePost|isLocked" "SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift"` 실행, 조건식 확인. +- `git diff --check`, `plutil -lint "SodaLive.xcodeproj/project.pbxproj"`, `SodaLive-dev` Debug 빌드 통과. +- 실제 화면 상태 수동 확인은 미실행이며 잔여 확인 사항으로 남김. + +### 2차 통합 재점검 — 2026-07-31 + +- `REV-P15-001`은 `price <= 0 || existOrdered`로 수정된 현재 구현을 확인해 **수정 완료**로 판정했다. + +| ID | 심각도 | 상태 | 제목 | 소유 Task | +|---|---|---|---|---| +| `REV-P15-002` | Medium | 확정 | 이미지 없는 유료 미구매 게시물은 구매 UI와 기존 구매 callback에 진입할 수 없음 | `Task 15.3` / `P15-R2` | + +#### REV-P15-002 — 이미지 없는 유료 미구매 게시물 구매 진입 누락 + +- **관찰 내용:** 카드는 `hasImage`일 때만 `imageContent`를 만들고, 잠금 표시와 `onTapPurchase` Button은 `imageContent` 내부에만 있다. 따라서 `imageUrl == nil && price > 0 && existOrdered == false`이면 `isLocked == true`여도 구매 UI가 렌더링되지 않는다. +- **영향:** PRD가 허용한 이미지 없는 유료 게시물은 추천 피드에서 구매할 방법이 없고, `isAccessiblePost == false`라 반응 UI도 표시되지 않는다. +- **근거:** `CommunityPostCard.swift:65-76`, `:125-173`, `:222-236`; PRD 7.4의 이미지 nullable 및 유료 미구매 구매 지원 기준. +- **재현:** `imageUrl: nil`, `price: 100`, `existOrdered: false` fixture로 카드를 구성하면 본문 아래에 이미지·잠금·구매 Button이 모두 없다. +- **권장 조치:** 이미지 없는 레이아웃을 유지한 채 잠긴 상태의 구매 affordance만 제공하고 기존 `onTapPurchase` 흐름을 재사용한다. +- **계획 전환:** `plan-task.md` Phase 15에 `Task 15.3` / `P15-R2`를 추가했다. + +#### REV-P15-002 수정 기록 — 2026-07-31 + +- `CommunityPostCard`에서 이미지가 없고 유료 미구매인 경우에도 본문 아래에 잠금/가격 구매 affordance를 표시하고 기존 `onTapPurchase` callback을 재사용하도록 수정했다. +- 이미지가 있는 유료 미구매 상태의 `paidLockOverlay`, 무료·구매 완료 상태의 reaction bar 조건은 변경하지 않았다. +- `rg -n 'hasImage|isLocked|lockedTextPurchaseButton|paidLockOverlay|purchaseCommunityPost|onTapPurchase' SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift SodaLive/Sources/V2/Main/Home/Recommendation`로 연결을 확인했다. +- Oracle 리뷰어 게이트에서 기존 구매 flow 재사용과 차단/중요 findings 없음 판정을 받았다. +- 실제 화면에서 이미지 없는 유료 미구매 fixture의 구매 callback 1회 호출 수동 확인은 잔여 확인 사항으로 남긴다. + +**현재 결론:** `REV-P15-001`, `REV-P15-002` 수정 완료 diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase16-business-info-section.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase16-business-info-section.md new file mode 100644 index 00000000..1b54e8ef --- /dev/null +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase16-business-info-section.md @@ -0,0 +1,123 @@ +# Phase 16 사업자 정보 섹션 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 16 / Task 16.1 | +| 기준 commit 또는 working tree | `3ae2dc19d67d0a88bc5043f970e4cfabdfb28b60` + 현재 working tree | +| 리뷰 일자 | 2026-07-30 | +| 리뷰어 | Codex | +| 기준 문서 | PRD 7.5·8.3, `plan-task.md` Phase 16 | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +- 3줄 말줄임, 더보기/접기, width와 공용 V2 컴포넌트 재사용 여부를 확인한다. +- 기존 Audition 화면의 legacy `CustomView/ExpandableTextView` 동작 변경은 제외한다. + +## 3. 판정 기준 + +- 사용자 기능은 충족하더라도 계획한 공용 구현이 빌드되지 않고 같은 코드가 복제되면 Low 유지보수성/문서 정합성 결함으로 판정한다. + +## 4. 검토한 근거와 실행한 검증 + +- 코드: `MainHomeBusinessInfoSection.swift:3-67`, `V2/Component/Text/ExpandableTextView.swift:3-71` +- 프로젝트: `project.pbxproj`의 두 `ExpandableTextView.swift` file reference와 Sources 등록 +- 환경: macOS 26.0, Xcode 26.0, Swift 6.2 + +| 명령 | 결과 | 핵심 증거 | +|---|---|---| +| `rg "BusinessInfoExpandableTextView\|ExpandableTextView" ...` | 실패 발견 | 화면 파일에 측정/토글 구현이 복제됨 | +| `rg "C922F89A...\|ExpandableTextView.swift in Sources" project.pbxproj` | 실패 발견 | V2 파일은 file reference만 있고 Sources 등록 없음 | +| 최신 `xcodebuild ... build` compile 목록 확인 | 실패 근거 확인 | legacy `CustomView/ExpandableTextView.swift`만 app target에서 컴파일 | +| `xcodebuild ... -derivedDataPath "/private/tmp/SodaLiveDerivedDataReview20260730" build` | 성공 | private 복제 구현 때문에 화면 자체는 컴파일 성공 | + +## 5. 발견 사항 요약 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | +|---|---|---|---|---| +| `REV-P16-001` | Low | 확정 | 공용 V2 확장 텍스트는 미빌드 상태이고 화면에 동일 구현이 복제됨 | `Task 16.2` / `P16-R1` | + +## 6. 발견 사항 상세 + +### REV-P16-001 — 확장 텍스트 구현과 project target 불일치 + +- **심각도:** Low +- **상태:** 확정 +- **소유 Task:** `Task 16.2` + +**관찰 내용** + +PRD는 `MainHomeBusinessInfoSection`이 재사용 가능한 V2 `ExpandableTextView`를 소비하도록 정했다. 실제 화면은 private `BusinessInfoExpandableTextView`를 다시 구현하며, V2 공용 파일은 Xcode group에만 있고 Sources에 포함되지 않는다. 반면 같은 타입명의 legacy CustomView 파일은 앱 타깃에서 컴파일된다. + +**영향** + +높이 측정/토글 수정이 두 V2 파일에 중복되고, 공용 컴포넌트 수정이 실제 앱에 반영되지 않아 검증 기록과 빌드 상태가 어긋난다. + +**권장 조치** + +legacy 소비처는 유지하면서 타입명 충돌을 피하고, 빌드 대상 공용 V2 구현 하나로 화면을 연결한 뒤 이번 변경이 만든 private 중복만 제거한다. + +**판정 기록** + +- 2026-07-30 — source 내용, PBX Sources와 실제 SwiftCompile 목록 대조로 확정. + +## 7. 확정 항목의 plan·goal 전환 + +`plan-task.md` Phase 16에 `Task 16.2` / `P16-R1`을 추가했다. + +## 8. 리뷰 종료 판정 + +| 항목 | 결과 | 근거 | +|---|---|---| +| 범위 확인 | 충족 | source/project/build 대조 | +| 후보 판정 | 충족 | `REV-P16-001` 확정 | +| plan 반영 | 충족 | Task 16.2 | +| 검증 기록 | 충족 | 4절 | + +**최종 결론:** 수정 goal 필요 + +## 9. 수정 후 검증 기록 + +- 2026-07-30 — `MainHomeBusinessInfoSection`의 private `BusinessInfoExpandableTextView` 중복 구현을 제거하고 V2 공용 `V2ExpandableTextView`를 사용하도록 수정. +- legacy `CustomView/ExpandableTextView` 소비처와 산출물 충돌을 피하기 위해 V2 공용 파일명을 `V2ExpandableTextView.swift`로 정리하고 `SodaLive.xcodeproj/project.pbxproj` Sources에 포함. +- 중간 빌드에서 V2/legacy 파일명이 모두 `ExpandableTextView.swift`일 때 `Multiple commands produce ... ExpandableTextView.stringsdata` 실패를 확인했고, 파일명 정리 후 재빌드 성공. +- `rg -n "V2ExpandableTextView|BusinessInfoExpandableTextView|struct ExpandableTextView|struct V2ExpandableTextView" ...` 실행, 사업자 섹션은 V2 공용 구현을 사용하고 legacy 타입은 기존 경로에만 남음 확인. +- `git diff --check`, `plutil -lint "SodaLive.xcodeproj/project.pbxproj"`, `SodaLive-dev` Debug 빌드 통과. +- 실제 3줄 이하/초과 화면 수동 확인은 미실행이며 잔여 확인 사항으로 남김. + +### 2차 통합 재점검 — 2026-07-31 + +- `REV-P16-001`은 사업자 정보 섹션이 `V2ExpandableTextView` 하나를 소비하는 현재 구현을 확인해 **수정 완료**로 판정했다. + +| ID | 심각도 | 상태 | 제목 | 소유 Task | +|---|---|---|---|---| +| `REV-P16-002` | Low | 확정 | `V2ExpandableTextView.swift`가 프로덕션 target Sources에 두 번 등록됨 | `Task 16.3` / `P16-R2` | + +#### REV-P16-002 — 프로덕션 Sources 중복 등록 + +- **관찰 내용:** `SodaLive` target의 Sources phase `40FE0E80`에 동일 file reference `C922F89A`를 가리키는 build file `C922F89B`, `C922F89D`가 함께 있다. `SodaLive-dev` Sources phase `40FE0E9F`에는 `C922F89C` 한 건만 있다. +- **영향:** 프로덕션 프로젝트 메타데이터에 동일 Swift 소스가 중복되어 빌드 경고 또는 이후 프로젝트 수정 시 불필요한 충돌 원인이 된다. +- **근거:** `project.pbxproj:6385-6434`, `:6846`, `:7760`, `:8027`; 현재 staged diff에서 Task 16.2가 세 build file을 추가한 내역. +- **재현:** 두 앱 target의 Sources phase에서 `V2ExpandableTextView.swift in Sources`를 세면 `SodaLive=2`, `SodaLive-dev=1`이다. +- **권장 조치:** `SodaLive`의 중복 build file 한 건만 제거하고 각 target 포함 수를 1로 맞춘다. 기존 다른 Sources 중복은 이번 범위에서 변경하지 않는다. +- **계획 전환:** `plan-task.md` Phase 16에 `Task 16.3` / `P16-R2`를 추가했다. +- **빌드 확인:** `xcodebuild -quiet -workspace SodaLive.xcworkspace -scheme SodaLive -configuration Debug -derivedDataPath /private/tmp/SodaLiveDerivedDataReviewCurrentProd build 2>&1 | rg 'V2ExpandableTextView|BUILD'`에서 `Skipping duplicate build file ... V2ExpandableTextView.swift` 경고를 재현했고 빌드 종료 코드는 0이었다. + +#### REV-P16-002 수정 기록 — 2026-07-31 + +- `SodaLive` target Sources phase에 중복 등록된 `C922F89D /* V2ExpandableTextView.swift in Sources */` 한 건과 해당 `PBXBuildFile` 정의를 제거했다. +- `SodaLive-dev` target의 `C922F89C` 등록과 다른 Sources 항목은 변경하지 않았다. +- `rg -n 'V2ExpandableTextView\.swift in Sources|C922F89D' SodaLive.xcodeproj/project.pbxproj`로 `C922F89B`, `C922F89C` 두 건만 남고 `C922F89D`가 제거된 것을 확인했다. +- `plutil -lint SodaLive.xcodeproj/project.pbxproj` 결과 `OK`를 확인했다. +- 전체 빌드는 도구 시간 제한으로 완료하지 못했으나 Oracle 리뷰어 게이트에서 PBX 등록 조건에 대한 차단/중요 findings 없음 판정을 받았다. + +**현재 결론:** `REV-P16-001`, `REV-P16-002` 수정 완료 + +### 3차 통합 재점검 — 2026-07-31 + +- PBX Sources phase에서 `V2ExpandableTextView.swift`가 `SodaLive`, `SodaLive-dev` target에 각각 한 번만 등록된 것을 다시 확인했다. +- 최신 `SodaLive` Debug 빌드는 종료 코드 0으로 성공했고 `V2ExpandableTextView.swift` duplicate build warning은 더 이상 출력되지 않았다. +- 최신 `SodaLive-dev` 재실행은 10분 이상 종료되지 않아 중단했으며, dev target 등록을 변경하지 않은 점과 직전 dev 빌드 성공 기록을 함께 유지한다. +- Phase 16의 `REV-P16-001`, `REV-P16-002` 수정 완료 판정은 유지한다. 다른 홈 추천 파일의 중복은 Phase 18 `REV-P18-001`이 소유한다. diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase17-section-assembly.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase17-section-assembly.md new file mode 100644 index 00000000..32871a6d --- /dev/null +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase17-section-assembly.md @@ -0,0 +1,66 @@ +# Phase 17 섹션 조립과 빈 상태 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 17 / Task 17.1 및 후속 장르 미표시 결정 | +| 기준 commit 또는 working tree | `3ae2dc19d67d0a88bc5043f970e4cfabdfb28b60` + 현재 working tree | +| 리뷰 일자 | 2026-07-30 | +| 리뷰어 | Codex | +| 기준 문서 | `plan-task.md` Phase 17과 2026-07-12 재분류 원칙 | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +- 최신 결정 기준 섹션 순서와 데이터 기반 섹션의 빈 배열 숨김을 확인한다. +- 과거 검증 기록은 사실 이력으로 보존하고 현재 기준과 구분한다. + +## 3. 판정 기준 + +- 후속 2026-07-12 미표시 결정이 기존 Task 17.1보다 최신이므로 현재 코드는 장르 섹션을 조립하지 않아야 한다. + +## 4. 검토한 근거와 실행한 검증 + +- 코드: `MainHomeRecommendationView.swift:25-83`, 각 섹션의 `if !items.isEmpty` +- 이력: commit `47fe0e7` +- 환경: macOS 26.0, Xcode 26.0, Swift 6.2 + +| 명령 | 결과 | 핵심 증거 | +|---|---|---| +| 섹션 타입 순서 `rg` | 성공 | live→banner→active→debut→AI→cheer→community→business | +| `rg "MainHomeGenreCreatorSection\|genreCreators" MainHomeRecommendationView.swift` | 성공 | 검색 결과 없음 | +| 각 데이터 섹션 empty guard 확인 | 성공 | 빈 배열에서 제목/컨테이너 미표시 | +| `xcodebuild ... -derivedDataPath "/private/tmp/SodaLiveDerivedDataReview20260730" build` | 성공 | `BUILD SUCCEEDED` | + +## 5. 발견 사항 요약 + +**코드 확정 발견 사항 없음.** + +문서의 활성 기준 충돌은 `REV-P19-001`이 소유한다. + +## 6. 발견 사항 상세 + +현재 코드는 최신 장르 미표시 결정을 충족한다. Task 17.1 본문과 PRD 화면 트리가 과거 노출 순서를 유지하는 문제는 코드 수정 대상이 아니므로 Phase 19로 전환했다. + +## 7. 확정 항목의 plan·goal 전환 + +이 Phase의 신규 Task 없음. 문서 정정은 `Task 19.2` / `P19-R1`에서 수행한다. + +## 8. 리뷰 종료 판정 + +| 항목 | 결과 | 근거 | +|---|---|---| +| 코드 범위 확인 | 충족 | 순서와 empty guard 확인 | +| 후보 판정 | 충족 | 코드 후보 없음 | +| 문서 불일치 전환 | 충족 | REV-P19-001 / Task 19.2 | +| 검증 기록 | 충족 | 4절 | + +**최종 결론:** 코드 확정 발견 사항 없음 + +## 9. 수정 후 검증 기록 + +### 2차 통합 재점검 — 2026-07-31 + +- 활성 섹션 순서와 빈 배열 숨김, 장르 섹션 미조립을 현재 PRD·코드와 다시 대조했다. +- `REV-P19-001` 문서 충돌은 수정 완료이며 Phase 17 코드의 추가 확정 발견 사항 없음. diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase18-final-project-build.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase18-final-project-build.md new file mode 100644 index 00000000..b7fa8317 --- /dev/null +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase18-final-project-build.md @@ -0,0 +1,158 @@ +# Phase 18 최종 프로젝트 포함과 빌드 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 18 / Task 18.1~18.2 | +| 기준 commit 또는 working tree | `3ae2dc19d67d0a88bc5043f970e4cfabdfb28b60` + 현재 working tree | +| 리뷰 일자 | 2026-07-30 | +| 리뷰어 | Codex | +| 기준 문서 | `plan-task.md` Phase 18, 빌드/검증 가이드 | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +- 프로젝트 유효성, 금지 범위, 공백 오류와 현재 `SodaLive-dev` 전체 빌드를 확인한다. +- 개별 컴포넌트 target 누락의 기능 소유권은 해당 구현 Phase에서 판정한다. + +## 3. 판정 기준 + +- `plutil`, 금지 문자열, `git diff --check`, app target compile/link/sign/validate 성공을 기준으로 판정한다. + +## 4. 검토한 근거와 실행한 검증 + +- 환경: macOS 26.0, Xcode 26.0, Any iOS Device / Debug + +| 명령 | 결과 | 핵심 증거 | +|---|---|---| +| `plutil -lint SodaLive.xcodeproj/project.pbxproj` | 성공 | `OK` | +| 제외 섹션/Figma URL/기존 HomeApi endpoint 검색 | 성공 | 검색 결과 없음 | +| `git diff --check` | 성공 | 출력 없음 | +| sandbox 내 `xcodebuild -workspace ... -list` | 환경 실패 | CoreSimulator/캐시 권한 제한 | +| sandbox 밖 `xcodebuild ... -derivedDataPath "/private/tmp/SodaLiveDerivedDataReview20260730" build` | 성공 | `BUILD SUCCEEDED` | + +## 5. 발견 사항 요약 + +**Phase 18 공통 Gate의 추가 확정 발견 사항 없음.** + +V2 공용 `ExpandableTextView`가 Sources에 포함되지 않은 구체적 문제는 `REV-P16-001` / `Task 16.2`가 소유한다. + +## 6. 발견 사항 상세 + +전체 앱은 private 사업자 정보 구현을 사용하므로 빌드가 성공한다. 따라서 project 전체 Gate 실패는 아니지만, 의도한 공용 파일의 미포함은 Phase 16에서 별도로 수정해야 한다. + +## 7. 확정 항목의 plan·goal 전환 + +Phase 18 신규 Task 없음. `Task 16.2`에 project 포함 검증을 명시했다. + +## 8. 리뷰 종료 판정 + +| 항목 | 결과 | 근거 | +|---|---|---| +| 공통 Gate | 충족 | lint/static/build 성공 | +| 후보 판정 | 충족 | 구체적 누락은 REV-P16-001로 전환 | +| plan 반영 | 충족 | Task 16.2 | +| 검증 기록 | 충족 | 4절 | + +**최종 결론:** 공통 빌드 Gate 통과, `REV-P16-001` 후속 수정 필요 + +## 9. 수정 후 검증 기록 + +Phase 16 수정 후 Phase 18 build를 재실행해 누적한다. + +### 2차 통합 재점검 — 2026-07-31 + +- `REV-P16-001` 보완 후 `SodaLive-dev` Debug 빌드 성공 기록과 프로덕션 `SodaLive` Debug 빌드를 다시 확인했다. +- `xcodebuild -workspace SodaLive.xcworkspace -scheme SodaLive -configuration Debug -derivedDataPath /private/tmp/SodaLiveDerivedDataReviewCurrentProd build` — 종료 코드 0, `BUILD SUCCEEDED`. +- 프로덕션 빌드는 `V2ExpandableTextView.swift` 중복 Compile Sources 경고를 출력했으며, 정적 PBX 대조에서도 `SodaLive=2`, `SodaLive-dev=1` 등록을 확인했다. +- 해당 경고는 Phase 16 `REV-P16-002` / `Task 16.3`으로 전환했다. 함께 출력된 다른 기존 Sources 중복 경고는 이번 추천 홈 변경 범위 밖이므로 수정하지 않았다. + +**현재 결론:** 두 앱 scheme의 빌드 성공 근거는 충족, `REV-P16-002` 후속 수정 필요 + +### 3차 통합 재점검 — 2026-07-31 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | +|---|---|---|---|---| +| `REV-P18-001` | Low | 확정 | 프로덕션 target에 홈 추천 범위 Swift 파일 11개가 중복 Sources 등록됨 | `Task 18.3` / `P18-R1` | + +#### REV-P18-001 — 홈 추천 Compile Sources 중복 등록 + +- **관찰 내용:** `SodaLive` Sources phase에는 이번 계획 범위의 11개 Swift file reference가 각각 두 번 포함되고 `SodaLive-dev`에는 한 번씩 포함된다. 최신 프로덕션 빌드도 해당 파일들의 `Skipping duplicate build file in Compile Sources` 경고를 출력한다. +- **영향:** 빌드는 성공하지만 Phase 18의 프로젝트 포함 검증이 중복 없는 target 구성을 보장하지 못하며, Xcode 프로젝트 수정 시 불필요한 충돌과 경고가 계속 발생한다. +- **근거:** `SodaLive.xcodeproj/project.pbxproj`의 `40FE0E802A8361DF00252293 /* Sources */`, `40FE0E9F2A8362A200252293 /* Sources */`; 2026-07-31 `SodaLive` Debug 빌드 로그. +- **대상 파일:** `MainHomeView.swift`, `MainHomeRecommendationView.swift`, `MainHomeRecommendationViewModel.swift`, `MainHomeRankingView.swift`, `MainHomeFollowingView.swift`, `MainHomeLiveSection.swift`, `MainHomeBannerSection.swift`, `MainHomeActiveCreatorSection.swift`, `MainHomeRecentDebutCreatorSection.swift`, `MainHomeAiCharacterSection.swift`, `MainHomeBusinessInfoSection.swift`. +- **재현:** 두 앱 target의 Sources phase에서 위 file reference를 세면 `SodaLive=2`, `SodaLive-dev=1`이며 프로덕션 빌드 로그에서 같은 경고를 확인할 수 있다. +- **권장 조치:** 프로덕션 중복 build file 한 건씩만 제거하고 두 scheme 빌드와 경고 부재를 확인한다. 다른 기존 중복 Sources는 이번 Task 범위에서 변경하지 않는다. +- **계획 전환:** `plan-task.md` Phase 18에 `Task 18.3` / `P18-R1`을 추가했다. + +검증 기록: + +- `plutil -lint SodaLive.xcodeproj/project.pbxproj` — `OK`. +- `git diff --check` — 성공, 출력 없음. +- `xcodebuild -quiet -workspace SodaLive.xcworkspace -scheme SodaLive -configuration Debug -derivedDataPath /private/tmp/SodaLiveDerivedDataReviewCurrentProd build` — 종료 코드 0, 빌드 성공. `V2ExpandableTextView.swift` 중복 경고는 제거됐지만 `REV-P18-001` 대상 경고를 확인했다. +- 최신 `SodaLive-dev` 재실행은 10분 이상 종료되지 않아 중단했고, 같은 dev target의 직전 현재 변경군 빌드 성공 기록은 유지한다. + +**당시 결론:** 빌드는 성공하지만 `REV-P18-001` 수정 goal 필요 + +### REV-P18-001 수정 기록 — 2026-07-31 + +- `SodaLive.xcodeproj/project.pbxproj`에서 홈 추천 범위 11개 Swift 파일의 중복 Compile Sources 등록을 정리했다. +- 대상 파일 `MainHomeView.swift`, `MainHomeRecommendationView.swift`, `MainHomeRecommendationViewModel.swift`, `MainHomeRankingView.swift`, `MainHomeFollowingView.swift`, `MainHomeLiveSection.swift`, `MainHomeBannerSection.swift`, `MainHomeActiveCreatorSection.swift`, `MainHomeRecentDebutCreatorSection.swift`, `MainHomeAiCharacterSection.swift`, `MainHomeBusinessInfoSection.swift`는 `SodaLive`, `SodaLive-dev` target에 각각 1회 등록된다. +- `plutil -lint SodaLive.xcodeproj/project.pbxproj` — `OK`. +- `xcodebuild -workspace SodaLive.xcworkspace -list` — 프로젝트 파싱 성공, `SodaLive`와 `SodaLive-dev` scheme 확인. +- `xcodebuild -quiet -workspace SodaLive.xcworkspace -scheme SodaLive-dev -configuration Debug -derivedDataPath /var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataTask183DevRetry build` — 종료 코드 0. +- `xcodebuild -quiet -workspace SodaLive.xcworkspace -scheme SodaLive -configuration Debug -derivedDataPath /var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataTask183ProdFinal build` — 종료 코드 0. +- 두 빌드 로그에서 `REV-P18-001` 대상 11개 홈 추천 파일의 duplicate build warning은 검색되지 않았다. 기존 범위 밖 V2 파일 duplicate warning은 남아 있으며 이번 Task에서 변경하지 않았다. +- `git diff --check` — 성공, 출력 없음. + +**현재 결론:** `REV-P18-001` 수정 완료 + +### 4차 최종 재점검 — 2026-07-31 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | +|---|---|---|---|---| +| `REV-P18-002` | Low | 확정 | `Task 18.3` 이후에도 홈 추천 파일 11개의 프로덕션 Sources 중복이 남음 | `Task 18.4` / `P18-R2` | + +#### REV-P18-002 — 프로덕션 Sources 잔여 중복 + +- **관찰 내용:** `Task 18.3` 수정 기록은 대상 11개 파일이 두 target에 각각 1회 등록됐다고 판정하지만, 현재 `40FE0E802A8361DF00252293 /* Sources */`에는 각 파일이 2회, `40FE0E9F2A8362A200252293 /* Sources */`에는 각 파일이 1회 존재한다. 기존 수정은 프로덕션 3개 등록 중 한 건만 제거해 2개 등록을 남겼다. +- **영향:** 프로덕션 빌드가 대상 11개 파일 모두에 `Skipping duplicate build file in Compile Sources` 경고를 계속 출력하고, Phase 18 완료 증거와 실제 프로젝트 상태가 일치하지 않는다. +- **대상 파일:** `MainHomeView.swift`, `MainHomeRecommendationView.swift`, `MainHomeRecommendationViewModel.swift`, `MainHomeRankingView.swift`, `MainHomeFollowingView.swift`, `MainHomeLiveSection.swift`, `MainHomeBannerSection.swift`, `MainHomeActiveCreatorSection.swift`, `MainHomeRecentDebutCreatorSection.swift`, `MainHomeAiCharacterSection.swift`, `MainHomeBusinessInfoSection.swift`. +- **재현 또는 검증:** 두 PBXSourcesBuildPhase 범위의 파일명 포함 수를 세면 대상 전체가 `SodaLive=2`, `SodaLive-dev=1`이며, 격리 DerivedData의 프로덕션 빌드에서도 11개 경고가 재현된다. +- **권장 조치:** `SodaLive`의 잔여 중복 build file과 Sources 참조 한 건씩만 제거하고 dev target과 범위 밖 중복은 보존한 뒤 두 scheme을 fresh DerivedData로 검증한다. +- **계획 전환:** `plan-task.md` Phase 18에 `Task 18.4` / `P18-R2`를 추가했다. + +검증 기록: + +- `git diff --check` — 성공, 출력 없음. +- `plutil -lint SodaLive.xcodeproj/project.pbxproj` — `OK`. +- PBX Sources phase 대조 — 대상 11개 파일 모두 `SodaLive=2`, `SodaLive-dev=1`. +- 격리 DerivedData 빌드 — `SodaLive`, `SodaLive-dev` 모두 종료 코드 0. +- 빌드 경고 대조 — `SodaLive`에서는 대상 11개 파일의 duplicate build warning이 모두 재현됐고 `SodaLive-dev`에서는 대상 경고가 없었다. +- 첫 프로덕션 빌드 시도는 기본 DerivedData의 기존 `build.db` lock으로 종료 코드 65였으며, 위 결과는 별도 격리 DerivedData에서 다시 검증한 결과다. + +**당시 결론:** `REV-P18-001`의 수정 완료 판정을 정정하며 `REV-P18-002` 수정 goal 필요 + +### REV-P18-002 수정 기록 — 2026-07-31 + +- `SodaLive.xcodeproj/project.pbxproj`의 `SodaLive` Sources phase에서 홈 추천 범위 11개 Swift 파일의 잔여 중복 build file 참조를 제거했다. +- 대상 파일 `MainHomeView.swift`, `MainHomeRecommendationView.swift`, `MainHomeRecommendationViewModel.swift`, `MainHomeRankingView.swift`, `MainHomeFollowingView.swift`, `MainHomeLiveSection.swift`, `MainHomeBannerSection.swift`, `MainHomeActiveCreatorSection.swift`, `MainHomeRecentDebutCreatorSection.swift`, `MainHomeAiCharacterSection.swift`, `MainHomeBusinessInfoSection.swift`는 `SodaLive`, `SodaLive-dev` target에 각각 1회 등록된다. +- `plutil -lint "SodaLive.xcodeproj/project.pbxproj"` — `OK`. +- `git diff --check` — 성공, 출력 없음. +- `xcodebuild -quiet -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -destination "generic/platform=iOS" -derivedDataPath "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataTask184DevRetry" build` — 종료 코드 0. +- `xcodebuild -quiet -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug -destination "generic/platform=iOS" -derivedDataPath "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataTask184Prod" build` — 종료 코드 0. +- 두 빌드 로그에서 `REV-P18-002` 대상 11개 홈 추천 파일의 duplicate build warning은 검색되지 않았다. +- 기존 범위 밖 `MainHomeRankingViewModel.swift`, `MainHomeFollowingViewModel.swift` duplicate warning은 `SodaLive` 빌드 로그에 남아 있으며 이번 Task에서 변경하지 않았다. +- 첫 `SodaLive-dev` 빌드 시도는 10분 제한으로 `BUILD INTERRUPTED`됐고, 위 결과는 동일 변경을 별도 DerivedData에서 다시 검증한 결과다. + +**현재 결론:** `REV-P18-002` 수정 완료 + +### 5차 최종 재점검 — 2026-07-31 + +- `plutil -lint SodaLive.xcodeproj/project.pbxproj`, `git diff --check`, `git diff --cached --check`가 모두 성공했다. +- clean DerivedData에서 `SodaLive-dev` Debug 빌드와 동일 DerivedData의 `SodaLive` Debug 빌드가 각각 종료 코드 0으로 완료됐다. +- 두 빌드 로그에 `REV-P18-002` 대상 홈 추천 11개 파일의 duplicate build warning은 없었다. +- 프로덕션 target에는 다른 V2 기능 파일의 기존 duplicate warning이 남아 있으나 Phase 18 회귀 수정 대상 11개와 무관하고 이번 계획 범위에서 생성한 변경이 아니므로 별도 기존 부채로 유지한다. + +**현재 결론:** Phase 18 대상 빌드 및 Sources Gate 통과 diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase19-documentation-records.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase19-documentation-records.md new file mode 100644 index 00000000..52b4d5f1 --- /dev/null +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase19-documentation-records.md @@ -0,0 +1,173 @@ +# Phase 19 문서와 검증 기록 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 19 / Task 19.1 및 누적 검증 기록 | +| 기준 commit 또는 working tree | `3ae2dc19d67d0a88bc5043f970e4cfabdfb28b60` + 현재 working tree | +| 리뷰 일자 | 2026-07-30 | +| 리뷰어 | Codex | +| 기준 문서 | `prd.md`, `plan-task.md`, 문서 정책 | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +- 요구사항 변경이 PRD→계획→검증 기록 순으로 누적됐는지, 현재 활성 기준과 코드가 일치하는지 확인한다. +- 과거 기록의 사실 여부를 재작성하지 않고 현재 기준 충돌만 판정한다. + +## 3. 판정 기준 + +- 후속 결정이 PRD와 활성 Task/Gate에 전파되지 않아 서로 반대 결과를 요구하면 Low 문서 정합성 결함으로 판정한다. + +## 4. 검토한 근거와 실행한 검증 + +- 문서: PRD Problem/화면 트리, 계획의 2026-07-12 결정, Phase 17, 2026-06-30 검증 기록 +- 코드: `MainHomeRecommendationView.swift` +- 환경: macOS 26.0, Xcode 26.0 + +| 명령 | 결과 | 핵심 증거 | +|---|---|---| +| `rg "장르의 크리에이터\|MainHomeGenreCreatorSection\|genreCreators" prd.md plan-task.md ...` | 실패 발견 | 최신 미표시 결정과 과거 활성 노출 기준이 공존 | +| `rg "MainHomeGenreCreatorSection\|genreCreators" MainHomeRecommendationView.swift` | 성공 | 현재 코드에는 참조 없음 | +| `git log -p -S'MainHomeGenreCreatorSection' -- MainHomeRecommendationView.swift` | 성공 | commit `47fe0e7`에서 의도적으로 제거 | +| `git diff --check` | 성공 | 출력 없음 | + +## 5. 발견 사항 요약 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | +|---|---|---|---|---| +| `REV-P19-001` | Low | 확정 | 장르 섹션 미표시 결정이 PRD와 Phase 17 활성 기준에 전파되지 않음 | `Task 19.2` / `P19-R1` | + +## 6. 발견 사항 상세 + +### REV-P19-001 — 장르 섹션 활성 문서 기준 충돌 + +- **심각도:** Low +- **상태:** 확정 +- **소유 Task:** `Task 19.2` + +**관찰 내용** + +계획 상단의 2026-07-12 결정과 현재 코드는 장르 섹션을 숨긴다. 그러나 PRD의 추천 화면 설명/트리와 Task 17.1 작업·기대 결과는 장르 섹션을 조립하도록 요구한다. 과거 2026-06-30 기록도 최신 결정과 구분하는 정정 기록이 없다. + +**영향** + +후속 구현자나 리뷰어가 어느 기준을 활성 요구사항으로 봐야 하는지 혼동하고, 현재 올바른 코드를 Phase 17 실패로 잘못 판정할 수 있다. + +**권장 조치** + +`genreCreators` 디코딩 필드 유지와 UI 미표시를 구분해 PRD와 활성 Phase 17 기준을 정정한다. 기존 Phase 13 구현/검증 기록은 삭제하지 않고 후속 정정만 누적한다. + +**판정 기록** + +- 2026-07-30 — 문서 세 위치, 현재 코드와 제거 commit 대조로 확정. + +## 7. 확정 항목의 plan·goal 전환 + +`plan-task.md` Phase 19에 `Task 19.2` / `P19-R1`을 추가했다. + +## 8. 리뷰 종료 판정 + +| 항목 | 결과 | 근거 | +|---|---|---| +| 문서 범위 확인 | 충족 | PRD/plan/history/code 대조 | +| 후보 판정 | 충족 | `REV-P19-001` 확정 | +| plan 반영 | 충족 | Task 19.2 | +| 검증 기록 | 충족 | 4절 | + +**최종 결론:** 수정 goal 필요 + +## 9. 수정 후 검증 기록 + +- 2026-07-30 — PRD와 Phase 17 활성 기준에서 장르 크리에이터 섹션 미표시 결정을 반영. +- `genreCreators` 응답 모델 필드는 API 디코딩을 위해 유지하되, 추천 탭 UI의 활성 화면 트리와 조립 검증 기준에서는 `MainHomeGenreCreatorSection`을 제외하도록 정정. +- 기존 Phase 13 구현 기록과 2026-06-30 검증 기록은 삭제하지 않고 최신 정정 기록만 누적. +- `rg -n "장르의 크리에이터|MainHomeGenreCreatorSection|genreCreators" "docs/20260602_메인_홈_추천_UI_API_연동" "SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift"` 실행, 최신 미표시 결정과 과거 기록 구분 확인. +- `git diff --check` 실행, 출력 없이 성공. + +### 2차 통합 재점검 — 2026-07-31 + +- `REV-P19-001`은 PRD와 Phase 17 활성 기준에 장르 섹션 미표시 결정이 반영된 현재 문서를 확인해 **수정 완료**로 판정했다. + +| ID | 심각도 | 상태 | 제목 | 소유 Task | +|---|---|---|---|---| +| `REV-P19-002` | Low | 확정 | PRD 응답 모델 Markdown code fence가 중첩·누락되어 이후 본문 렌더링이 깨짐 | `Task 19.3` / `P19-R2` | +| `REV-P19-003` | Low | 확정 | 활성 문서가 이름 변경 전 `ExpandableTextView.swift` 경로를 가리킴 | `Task 19.3` / `P19-R2` | + +#### REV-P19-002 — PRD 응답 예시 code fence 불균형 + +- **관찰 내용:** PRD 7.1은 line 66에서 연 `kotlin` fence를 `RecommendationBannerResponse` heading 전에 닫지 않고 line 117에서 새 `kotlin` fence를 연다. 그 결과 heading과 설명이 코드 블록으로 처리되고 이후 fence 경계도 어긋난다. +- **영향:** 렌더링된 PRD에서 응답 모델 구조와 설명 계층이 깨져 구현 기준을 오독할 수 있다. +- **근거:** `prd.md:66-148`. +- **권장 조치:** 모델 예시 단위로 fence를 정상 종료하되 모델 내용 자체는 변경하지 않는다. + +#### REV-P19-003 — 활성 V2 컴포넌트 경로 불일치 + +- **관찰 내용:** Task 16.2에서 공용 파일을 `V2ExpandableTextView.swift`로 이름 변경했지만 PRD의 구현 방향·재사용 후보와 계획 상단 주요 대상 파일은 여전히 `ExpandableTextView.swift`를 가리킨다. +- **영향:** 후속 작업자가 존재하지 않는 활성 경로를 기준 파일로 판단할 수 있다. +- **근거:** `prd.md:239`, `:306`, `plan-task.md:45`; 실제 파일 `SodaLive/Sources/V2/Component/Text/V2ExpandableTextView.swift`. +- **권장 조치:** 활성 기준만 최신 이름·경로로 정정하고 과거 Task/검증 이력은 보존한다. +- **계획 전환:** 두 항목을 `plan-task.md` Phase 19의 `Task 19.3` / `P19-R2`로 추가했다. + +#### REV-P19-002/REV-P19-003 수정 기록 — 2026-07-31 + +- PRD 7.1 응답 예시에서 `HomeRecommendationResponse`, `RecommendationBannerResponse`, `HomePopularCommunityPostItem` code fence를 모델 단위로 분리했다. +- PRD의 구현 방향과 재사용 후보, 계획 상단 주요 대상 파일의 활성 경로를 `SodaLive/Sources/V2/Component/Text/V2ExpandableTextView.swift`로 정정했다. +- 과거 Task와 검증 기록에 남은 이전 `ExpandableTextView.swift` 경로는 이력으로 보존했다. +- `rg -n '^```|#### RecommendationBannerResponse|#### HomePopularCommunityPostItem|V2/Component/Text/(ExpandableTextView|V2ExpandableTextView)\.swift|V2ExpandableTextView' docs/20260602_메인_홈_추천_UI_API_연동/prd.md docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md`로 PRD fence 10개와 활성 경로를 확인했다. +- Oracle 리뷰어 게이트에서 문서 정합성에 대한 차단/중요 findings 없음 판정을 받았다. + +**현재 결론:** `REV-P19-001`, `REV-P19-002`, `REV-P19-003` 수정 완료 + +### 3차 통합 재점검 — 2026-07-31 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | +|---|---|---|---|---| +| `REV-P19-004` | Low | 확정 | 활성 API 타입명과 Phase 11/21 후속 리뷰 기록이 현재 구현과 다름 | `Task 19.4` / `P19-R3` | + +#### REV-P19-004 — 활성 문서와 리뷰 근거 오기 + +- **관찰 내용:** PRD Goal은 실제 `MainHomeRecommendationApi` 대신 이동 전 이름인 `MainHomeApi`를 신규 타입으로 적는다. Phase 11의 2차 기록은 실제 전용 직사각형 카드 구현과 달리 `CreatorProfileGrid` 재사용을 확인했다고 적고, Phase 21의 3차 기록은 실제 `.liveReplay -> .content(targetId)` 분기와 달리 `LIVE_REPLAY`도 라이브/creator fallback을 사용한다고 적는다. +- **영향:** 후속 구현자가 존재하지 않는 타입을 찾거나, 리뷰 완료 근거를 실제 UI·라우팅과 반대로 해석할 수 있다. +- **근거:** `prd.md:18`, `phase11-recent-debut-creators.md:59`, `phase21-recent-activity-live-fallback.md:162`, 현재 Swift 구현. +- **권장 조치:** PRD 활성 타입명을 정정하고 두 리뷰의 기존 기록을 삭제하지 않은 채 사실관계 정정 기록을 누적한다. +- **계획 전환:** `plan-task.md` Phase 19에 `Task 19.4` / `P19-R3`을 추가했다. + +**당시 결론:** `REV-P19-004` 수정 goal 필요 + +#### REV-P19-004 수정 기록 — 2026-07-31 + +- PRD Goal의 활성 추천 API 타입명을 실제 구현과 같은 `MainHomeRecommendationApi`로 정정했다. +- Phase 11 리뷰의 `CreatorProfileGrid` 재사용 오기는 기존 기록을 삭제하지 않고 전용 `MainHomeRecentDebutCreatorItemView` 사용으로 append-only 정정 완료했다. +- Phase 21 리뷰의 `LIVE_REPLAY` fallback 오기는 기존 기록을 삭제하지 않고 `targetId` 기반 콘텐츠 상세 이동 및 creator fallback 없음으로 append-only 정정 완료했다. +- `rg -n "MainHomeApi|MainHomeRecommendationApi|CreatorProfileGrid|MainHomeRecentDebutCreatorItemView|LIVE_REPLAY|liveReplay|targetId" docs/20260602_메인_홈_추천_UI_API_연동/prd.md docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase11-recent-debut-creators.md docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase21-recent-activity-live-fallback.md`로 활성 타입명과 append-only 정정 기록을 확인했다. +- `git diff --check` — 성공, 출력 없음. + +**현재 결론:** `REV-P19-004` 수정 완료 + +### 4차 최종 재점검 — 2026-07-31 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | +|---|---|---|---|---| +| `REV-P19-005` | Low | 확정 | Phase 11 활성 완료 기준이 현재 전용 카드 구현과 최신 리뷰 판정에 반대됨 | `Task 19.5` / `P19-R4` | + +#### REV-P19-005 — 최근 데뷔 섹션 활성 계획 기준 충돌 + +- **관찰 내용:** `Task 11.1`은 `CreatorProfileGrid`와 `CreatorProfileItem` 우선 재사용 및 공용 widget 재사용을 기대 결과로 둔다. 현재 `MainHomeRecentDebutCreatorSection`은 Figma 직사각형 카드 요구 때문에 전용 `MainHomeRecentDebutCreatorItemView`를 사용하고, Phase 11 최신 리뷰도 이를 올바른 구현으로 판정한다. +- **영향:** Task는 완료 상태지만 활성 완료 기준만 적용하면 현재 올바르다고 판정한 코드를 실패로 보게 되어 후속 구현·리뷰 기준이 모순된다. +- **근거:** `plan-task.md:440-453`, `MainHomeRecentDebutCreatorSection.swift:20-69`, `phase11-recent-debut-creators.md` 3~5차 정정 기록. +- **재현 또는 검증:** `rg -n 'CreatorProfileGrid|MainHomeRecentDebutCreatorItemView' plan-task.md phase11-recent-debut-creators.md MainHomeRecentDebutCreatorSection.swift`로 활성 Task와 현재 코드·리뷰의 반대 기준을 확인한다. +- **권장 조치:** 공용 우선 원칙은 보존하되 기존 원형 widget이 Figma 직사각형 카드를 충족하지 못해 페이지 전용 View를 사용한다는 활성 예외와 현재 기대 결과를 `Task 11.1`에 명시한다. +- **계획 전환:** `plan-task.md` Phase 19에 `Task 19.5` / `P19-R4`를 추가했다. + +**현재 결론:** `REV-P19-005` 수정 goal 필요 + +#### REV-P19-005 수정 기록 — 2026-07-31 + +- `plan-task.md`의 활성 `Task 11.1` 작업 내용과 검증 기준을 현재 코드·Phase 11 최신 리뷰 결론에 맞게 정정했다. +- 공용 컴포넌트 우선 원칙은 유지하되, 공용 원형 `CreatorProfileGrid`/`CreatorProfileItem`으로 Figma 직사각형 카드 형태를 충족하지 못할 때 페이지 전용 `MainHomeRecentDebutCreatorItemView`를 사용하는 예외를 명시했다. +- 기존 Phase 11 완료 기록과 2차~5차 리뷰 정정 이력은 삭제하지 않고, Phase 11 리뷰에 6차 정정 기록을 누적했다. +- `rg -n "CreatorProfileGrid|MainHomeRecentDebutCreatorItemView" "docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md" "docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase11-recent-debut-creators.md" "SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeRecentDebutCreatorSection.swift"`와 `git diff --check`로 확인했다. + +**현재 결론:** `REV-P19-005` 수정 완료 diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase2-recommendation-api-response-model.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase2-recommendation-api-response-model.md new file mode 100644 index 00000000..f9db997c --- /dev/null +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase2-recommendation-api-response-model.md @@ -0,0 +1,103 @@ +# Phase 2 신규 API와 응답 모델 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 2 / Task 2.1~2.2 | +| 기준 commit 또는 working tree | `3ae2dc19d67d0a88bc5043f970e4cfabdfb28b60` + 현재 working tree | +| 리뷰 일자 | 2026-07-30 | +| 리뷰어 | Codex | +| 기준 문서 | PRD 7.1, `plan-task.md` Phase 2 | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +- 추천 전용 endpoint의 격리, 인증 패턴과 `HomeRecommendationResponse`의 PRD 계약 일치 여부를 확인한다. +- API/Repository/응답 모델을 포함하고 UI 렌더링은 제외한다. + +## 3. 판정 기준 + +- PRD에 없는 필수 `Decodable` 필드가 기준 응답을 실패시키면 High로 판정한다. +- Swift `Decodable` 동작, 실제 사용처와 전체 빌드로 후보를 검증한다. + +## 4. 검토한 근거와 실행한 검증 + +- 코드: `MainHomeRecommendationApi.swift:14-46`, `MainHomeRecommendationRepository.swift:6-15`, `MainHomeRecommendationResponse.swift:3-79` +- 문서: PRD의 `HomeRecommendationResponse` 스키마, Task 2.2의 과거 추정 필드 제거 기준 +- 환경: macOS 26.0, Xcode 26.0, Swift 6.2 + +| 명령 | 결과 | 핵심 증거 | +|---|---|---| +| `rg "/api/v2/home/recommendations\|getRecommendations" ...` | 성공 | endpoint는 추천 전용 Repository 경로에만 존재 | +| `rg "firstAudioContents\|HomeFirstAudioContentItem" ...` | 실패 발견 | 모델에만 남고 PRD와 소비 UI에는 없음 | +| `swift -module-cache-path /private/tmp/sodalive-review-swift-module-cache -e '<필수 Decodable 키 누락 재현>'` | 실패 재현 | `keyNotFound(firstAudioContents)` | +| `xcodebuild ... -derivedDataPath "/private/tmp/SodaLiveDerivedDataReview20260730" build` | 성공 | 컴파일은 성공하나 런타임 계약 실패를 검출하지 않음 | + +## 5. 발견 사항 요약 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | +|---|---|---|---|---| +| `REV-P2-001` | High | 확정 | PRD에 없는 `firstAudioContents` 필수 키가 추천 응답 전체 디코딩을 실패시킬 수 있음 | `Task 2.3` / `P2-R1` | + +## 6. 발견 사항 상세 + +### REV-P2-001 — PRD에 없는 필수 디코딩 키 + +- **심각도:** High +- **상태:** 확정 +- **소유 Task:** `Task 2.3` + +**관찰 내용** + +`HomeRecommendationResponse.firstAudioContents`는 non-optional이지만 현재 PRD 응답 스키마에 없고 앱에서도 소비되지 않는다. PRD 기준 JSON에 이 키가 없으면 `JSONDecoder`가 `keyNotFound`를 던져 다른 모든 추천 섹션도 표시되지 않는다. + +**근거** + +- 코드: `MainHomeRecommendationResponse.swift:8`, `:70-79` +- 문서: PRD 7.1 응답에는 `firstAudioContents`가 없음 +- 사용처: 모델 선언 외 검색 결과 없음 + +**재현** + +1. `lives` 등 PRD 필수 키를 포함하고 `firstAudioContents`를 제외한 JSON을 준비한다. +2. non-optional 동일 구조로 디코딩한다. +3. 실제 결과는 `keyNotFound(firstAudioContents)`다. +4. 요구 결과는 PRD 기준 응답의 정상 디코딩이다. + +**권장 조치** + +실제 사용 경로가 없는 필드와 전용 item 모델을 제거하고 PRD 최소 응답 디코딩 및 앱 빌드를 검증한다. + +**판정 기록** + +- 2026-07-30 — 코드, PRD, 사용처 검색과 Swift 디코딩 재현으로 확정. + +## 7. 확정 항목의 plan·goal 전환 + +`plan-task.md` Phase 2에 `Task 2.3` / `P2-R1`을 추가했다. + +## 8. 리뷰 종료 판정 + +| 판정 항목 | 결과 | 근거 | +|---|---|---| +| 리뷰 범위 전체 확인 | 충족 | API→Repository→모델 추적 | +| 후보 항목 판정 완료 | 충족 | `REV-P2-001` 확정 | +| 확정 항목 plan 반영 | 충족 | Task 2.3 | +| 검증 명령과 결과 기록 | 충족 | 4절 | + +**최종 결론:** 수정 goal 필요 + +**남은 항목:** `REV-P2-001` + +## 9. 수정 후 검증 기록 + +- 2026-07-30 — `HomeRecommendationResponse`에서 `firstAudioContents`와 `HomeFirstAudioContentItem` 제거. +- `swift -module-cache-path "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/sodalive-swift-module-cache" -e ''` 실행, `decoded without firstAudioContents` 확인. +- `rg -n "firstAudioContents|HomeFirstAudioContentItem" "SodaLive/Sources/V2/Main/Home/Recommendation"` 실행, 검색 결과 없음 확인. +- `git diff --check`, `plutil -lint "SodaLive.xcodeproj/project.pbxproj"`, `SodaLive-dev` Debug 빌드 통과. + +### 2차 통합 재점검 — 2026-07-31 + +- PRD 응답 필드와 현재 `HomeRecommendationResponse`를 다시 대조했다. +- `REV-P2-001`은 `firstAudioContents` 제거와 기준 JSON 디코딩 기록을 확인해 **수정 완료**로 판정했으며, 추가 확정 발견 사항 없음. diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase20-community-post-routing.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase20-community-post-routing.md new file mode 100644 index 00000000..26fa2318 --- /dev/null +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase20-community-post-routing.md @@ -0,0 +1,62 @@ +# Phase 20 커뮤니티 게시물 상세 라우팅 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 20 / Task 20.1~20.3 | +| 기준 commit 또는 working tree | `3ae2dc19d67d0a88bc5043f970e4cfabdfb28b60` + 현재 working tree | +| 리뷰 일자 | 2026-07-30 | +| 리뷰어 | Codex | +| 기준 문서 | PRD 7.3~7.4, `plan-task.md` Phase 20 | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +- 최근 활동 `COMMUNITY`, 인기 커뮤니티 카드/댓글의 `postId` 상세 이동과 다른 action 비회귀를 확인한다. +- 팔로잉 스케줄의 기존 `.community` route는 변경 제외 범위로 확인한다. + +## 3. 판정 기준 + +- 추천 흐름은 `.creatorChannelCommunityPostDetail(postId:)`, 팔로잉 스케줄은 기존 `.creatorCommunityAll(creatorId:)`를 유지해야 한다. + +## 4. 검토한 근거와 실행한 검증 + +- 코드: `HomeActiveCreatorTapDestination`, `MainHomePopularCommunitySection.swift:27-53`, `MainView.swift:539-543`, `:575-588` +- 환경: macOS 26.0, Xcode 26.0, Swift 6.2 + +| 명령 | 결과 | 핵심 증거 | +|---|---|---| +| `rg "onTapCommunityPost\|creatorChannelCommunityPostDetail\|onTapPost\|onTapComment" ...` | 성공 | 추천 `postId` 상세 연결 | +| `rg "handleFollowingScheduleTap\|creatorCommunityAll" MainView.swift` | 성공 | 팔로잉 기존 분기 유지 | +| `xcodebuild ... -derivedDataPath "/private/tmp/SodaLiveDerivedDataReview20260730" build` | 성공 | `BUILD SUCCEEDED` | + +## 5. 발견 사항 요약 + +**확정 발견 사항 없음.** + +## 6. 발견 사항 상세 + +확정 발견 사항 없음. + +## 7. 확정 항목의 plan·goal 전환 + +전환 항목 없음. + +## 8. 리뷰 종료 판정 + +| 항목 | 결과 | 근거 | +|---|---|---| +| 범위 확인 | 충족 | 모델→section→MainView route 추적 | +| 후보 판정 | 충족 | 남은 후보 없음 | +| plan 반영 | 해당 없음 | 확정 발견 사항 없음 | +| 검증 기록 | 충족 | 4절 | + +**최종 결론:** 확정 발견 사항 없음 + +## 9. 수정 후 검증 기록 + +### 2차 통합 재점검 — 2026-07-31 + +- 최근 활동 COMMUNITY와 인기 커뮤니티 카드가 `postId` 기준 상세 callback을 전달하고 공통 상세 guard를 재사용하는지 다시 확인했다. +- 추가 확정 발견 사항 없음. diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase21-recent-activity-live-fallback.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase21-recent-activity-live-fallback.md index b2d6108b..9849fd48 100644 --- a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase21-recent-activity-live-fallback.md +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase21-recent-activity-live-fallback.md @@ -145,3 +145,30 @@ Swift: 6.2 ## 9. 수정 후 검증 기록 해당 없음. 확정 발견 사항과 후속 수정이 없다. + +### 2차 통합 재점검 — 2026-07-30 + +- 기준: `3ae2dc19d67d0a88bc5043f970e4cfabdfb28b60` + 현재 working tree +- 무엇을: Phase 22 working tree 변경이 Phase 21의 `LIVE`, `LIVE_REPLAY`, `AUDIO`, `COMMUNITY`, unknown route와 기존 guard 연결을 회귀시키지 않았는지 재확인 +- 어떻게: + - `rg -n "activeCreatorTapDestination|onTapCreator|creatorId > 0|liveReplay|onTapLive|onTapContent|onTapCommunityPost" SodaLive/Sources/V2/Main/Home/Recommendation SodaLive/Sources/V2/Main/MainView.swift` — 분기와 callback 유지 확인 + - `git diff --check` — 성공, 출력 없음 + - `plutil -lint SodaLive.xcodeproj/project.pbxproj` — `OK` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -derivedDataPath "/private/tmp/SodaLiveDerivedDataReview20260730" build` — `BUILD SUCCEEDED` +- 결과: 기존 최종 결론 유지, 확정 발견 사항 없음 + +### 3차 통합 재점검 — 2026-07-31 + +- LIVE/LIVE_REPLAY에서 유효한 `targetId`는 라이브로, 무효 target은 creator 상세로 이동하는 fallback과 기존 guard를 다시 확인했다. +- 추가 확정 발견 사항 없음. + +### 4차 통합 재점검 — 2026-07-31 + +- 3차 기록은 `LIVE_REPLAY` 분기를 `LIVE`와 합쳐 서술한 오기다. 실제 구현과 활성 요구사항은 `LIVE + targetId`만 라이브룸, `LIVE + targetId == nil + creatorId > 0`만 creator 상세로 이동한다. +- `LIVE_REPLAY + targetId`는 콘텐츠 상세로 이동하고, `targetId == nil`이면 이동하지 않으며 creator fallback을 적용하지 않는다. +- append-only 리뷰 정정은 Phase 19 `REV-P19-004` / `Task 19.4`로 전환했다. Phase 21 production 코드의 추가 확정 발견 사항은 없다. + +### 5차 통합 재점검 — 2026-07-31 + +- `REV-P19-004` 보완으로 3차 기록의 `LIVE_REPLAY` fallback 오기는 위 4차 기록에서 append-only로 정정 완료했다. +- 현재 활성 결론은 `LIVE_REPLAY + targetId`만 콘텐츠 상세로 이동하고 `targetId == nil`이면 이동하지 않는 것이며, creator fallback은 적용하지 않는다. diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase22-follow-all-confirmation.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase22-follow-all-confirmation.md new file mode 100644 index 00000000..2018d3ad --- /dev/null +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase22-follow-all-confirmation.md @@ -0,0 +1,77 @@ +# Phase 22 모두 팔로우 확인 Dialog 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 22 / Task 22.1~22.3 | +| 기준 commit 또는 working tree | `3ae2dc19d67d0a88bc5043f970e4cfabdfb28b60` + 현재 working tree의 Phase 22 변경 | +| 리뷰 일자 | 2026-07-30 | +| 리뷰어 | Codex | +| 기준 문서 | PRD 7.6, `plan-task.md` Phase 22 | +| 리뷰 상태 | 판정 완료 / Phase Gate 완료 | + +## 2. 리뷰 목적과 범위 + +- 로그인 선행, 보류 action, 확인/취소/dimmed 처리, I18n과 기존 follow API 불변성을 확인한다. +- 실제 로그인 계정과 네트워크 요청 횟수를 사용하는 수동 검증까지 완료했다. + +## 3. 판정 기준 + +- 정적 흐름과 빌드는 코드 결함을 판정하고, 요청 0/1회와 언어별 표시는 수동 검증 결과로 판정한다. + +## 4. 검토한 근거와 실행한 검증 + +- 코드: `MainView.swift:27-30`, `:80-82`, `:332-358`, `:546-555`; `I18n.swift`의 follow confirm 문구 +- API: `MainHomeRecommendationViewModel.followAll`, 기존 request/Repository +- 환경: macOS 26.0, Xcode 26.0, Swift 6.2 + +| 명령 또는 수동 검증 | 결과 | 핵심 증거 | +|---|---|---| +| 상태/action/I18n `rg` | 성공 | 로그인 guard 뒤 dialog, 확인/취소 시 pending action 제거 | +| 기존 follow API/request `rg` | 성공 | endpoint/request/ViewModel 계약 변경 없음 | +| `git diff --check` | 성공 | 출력 없음 | +| `plutil -lint SodaLive.xcodeproj/project.pbxproj` | 성공 | `OK` | +| `xcodebuild ... -derivedDataPath "/private/tmp/SodaLiveDerivedDataReview20260730" build` | 성공 | `BUILD SUCCEEDED` | +| 실제 확인 전·취소·dimmed 요청 0회/확인 1회 | 성공 | 사용자 수동 테스트 결과 발견된 오류 없음 | +| ko/en/ja 화면 표시 | 성공 | 사용자 수동 테스트 결과 발견된 오류 없음 | + +## 5. 발견 사항 요약 + +**정적 리뷰에서 확정된 코드 결함 없음.** + +Task 22.3의 수동 Phase Gate까지 완료됐다. + +## 6. 발견 사항 상세 + +코드상 확인 시 pending action을 지역 변수로 옮긴 뒤 상태를 먼저 비우고 1회 실행하며, 취소와 dimmed는 action을 폐기한다. 비로그인은 action을 저장하지 않고 로그인으로 이동한다. + +수동 네트워크 횟수와 다국어 화면은 사용자 수동 테스트에서 발견된 오류가 없음을 완료 근거로 반영했다. + +## 7. 확정 항목의 plan·goal 전환 + +신규 전환 항목 없음. 기존 `Task 22.3`을 완료 처리했다. + +## 8. 리뷰 종료 판정 + +| 항목 | 결과 | 근거 | +|---|---|---| +| 정적 범위 확인 | 충족 | callback/I18n/API 추적 | +| 후보 판정 | 충족 | 정적 코드 후보 없음 | +| 수동 Phase Gate | 충족 | 요청 횟수/언어별 표시 수동 검증 완료 | +| 신규 plan 반영 | 해당 없음 | 추가 전환 항목 없음 | +| 자동 검증 기록 | 충족 | 4절 | + +**최종 결론:** 정적 확정 발견 사항 없음, Task 22.3 수동 검증 완료 + +**남은 항목:** 없음 + +## 9. 수정 후 검증 기록 + +- 2026-07-31 — 사용자 수동 테스트 결과 발견된 오류 없음. +- 확인 전·취소·dimmed 요청 0회, 확인 시 요청 1회, 비로그인 로그인 이동, 자동 팔로우 미실행, 기존 완료/호출 중/API 실패 toast 유지, ko/en/ja 문구 표시를 완료 근거로 반영. + +### 2차 통합 재점검 — 2026-07-31 + +- 수동 완료 증거와 확인 전 0회·확인 시 1회 요청, 비로그인·loading·오류·ko/en/ja 조건을 재검토했다. +- 추가 확정 발견 사항 및 남은 확인 항목 없음. diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase3-activity-type-i18n.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase3-activity-type-i18n.md new file mode 100644 index 00000000..9c248dc6 --- /dev/null +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase3-activity-type-i18n.md @@ -0,0 +1,62 @@ +# Phase 3 활동 타입과 I18n 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 3 / Task 3.1 | +| 기준 commit 또는 working tree | `3ae2dc19d67d0a88bc5043f970e4cfabdfb28b60` + 현재 working tree | +| 리뷰 일자 | 2026-07-30 | +| 리뷰어 | Codex | +| 기준 문서 | PRD 7.2, `plan-task.md` Phase 3 | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +- `LIVE`, `LIVE_REPLAY`, `AUDIO`, `COMMUNITY`, unknown 변환과 ko/en/ja 문구를 확인한다. +- 활동 타입 표시만 포함하고 탭 목적지는 Phase 21로 제외한다. + +## 3. 판정 기준 + +- 서버 코드 비노출, 확정 enum별 I18n 매핑과 빌드 성공을 기준으로 판정한다. + +## 4. 검토한 근거와 실행한 검증 + +- 코드: `RecommendedActivityType.swift:3-39`, `I18n.swift`의 `HomeRecommendation` +- 환경: macOS 26.0, Xcode 26.0, Swift 6.2 + +| 명령 | 결과 | 핵심 증거 | +|---|---|---| +| `rg "LIVE_REPLAY\|activityLive\|activityAudio\|activityCommunity\|ライブ\|オーディオ\|コミュニティ" ...` | 성공 | enum과 ko/en/ja 문구 확인 | +| `rg "case \\.unknown\|return \\"\\"" RecommendedActivityType.swift` | 성공 | unknown 서버 코드 비노출 | +| `xcodebuild ... -derivedDataPath "/private/tmp/SodaLiveDerivedDataReview20260730" build` | 성공 | `BUILD SUCCEEDED` | + +## 5. 발견 사항 요약 + +**확정 발견 사항 없음.** + +## 6. 발견 사항 상세 + +확정 발견 사항 없음. + +## 7. 확정 항목의 plan·goal 전환 + +전환 항목 없음. + +## 8. 리뷰 종료 판정 + +| 항목 | 결과 | 근거 | +|---|---|---| +| 범위 확인 | 충족 | enum/I18n 대조 | +| 후보 판정 | 충족 | 남은 후보 없음 | +| plan 반영 | 해당 없음 | 확정 발견 사항 없음 | +| 검증 기록 | 충족 | 4절 | + +**최종 결론:** 확정 발견 사항 없음 + +## 9. 수정 후 검증 기록 + +### 2차 통합 재점검 — 2026-07-31 + +- `RecommendedActivityType`의 LIVE/LIVE_REPLAY/AUDIO/COMMUNITY 및 unknown 표시와 ko/en/ja I18n 연결을 다시 확인했다. +- 추가 확정 발견 사항 없음. diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase4-recommendation-state-layer.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase4-recommendation-state-layer.md new file mode 100644 index 00000000..0c1944f4 --- /dev/null +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase4-recommendation-state-layer.md @@ -0,0 +1,99 @@ +# Phase 4 추천 데이터 상태 계층 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 4 / Task 4.1 | +| 기준 commit 또는 working tree | `3ae2dc19d67d0a88bc5043f970e4cfabdfb28b60` + 현재 working tree | +| 리뷰 일자 | 2026-07-30 | +| 리뷰어 | Codex | +| 기준 문서 | `plan-task.md` Phase 4 | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +- 추천 API 로딩, 성공 데이터, 오류, 모두 팔로우와 커뮤니티 action 상태가 전용 ViewModel에 모였는지 확인한다. +- ViewModel/Repository 연결을 포함하고 개별 UI 표현은 제외한다. + +## 3. 판정 기준 + +- `View → ViewModel → Repository → Api` 책임 경계, 실패 처리와 중복 요청 방지를 기준으로 판정한다. + +## 4. 검토한 근거와 실행한 검증 + +- 코드: `MainHomeRecommendationViewModel.swift:4-183`, `MainHomeRecommendationRepository.swift` +- 환경: macOS 26.0, Xcode 26.0, Swift 6.2 + +| 명령 | 결과 | 핵심 증거 | +|---|---|---| +| `rg "final class MainHomeRecommendationViewModel\|fetchRecommendations\|isLoading\|errorMessage\|followAll" SodaLive/Sources/V2/Main/Home` | 성공 | 전용 상태 계층 확인 | +| `rg "unowned self" MainHomeRecommendationViewModel.swift` | 성공 | 검색 결과 없음, weak 캡처 사용 | +| `xcodebuild ... -derivedDataPath "/private/tmp/SodaLiveDerivedDataReview20260730" build` | 성공 | `BUILD SUCCEEDED` | + +## 5. 발견 사항 요약 + +**확정 발견 사항 없음.** + +## 6. 발견 사항 상세 + +확정 발견 사항 없음. 응답 계약 결함은 `REV-P2-001`이 소유한다. + +## 7. 확정 항목의 plan·goal 전환 + +전환 항목 없음. + +## 8. 리뷰 종료 판정 + +| 항목 | 결과 | 근거 | +|---|---|---| +| 범위 확인 | 충족 | 상태와 Repository 흐름 추적 | +| 후보 판정 | 충족 | 남은 후보 없음 | +| plan 반영 | 해당 없음 | 확정 발견 사항 없음 | +| 검증 기록 | 충족 | 4절 | + +**최종 결론:** 확정 발견 사항 없음 + +## 9. 수정 후 검증 기록 + +### 2차 통합 재점검 — 2026-07-31 + +- 추천 조회·로딩·오류·mutation 상태의 `MainHomeRecommendationViewModel` 소유와 Repository 연결을 다시 확인했다. +- 추가 확정 발견 사항 없음. + +### 3차 최종 재점검 — 2026-07-31 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | +|---|---|---|---|---| +| `REV-P4-001` | Low | 확정 | 추천 응답 디코딩 오류가 프로젝트 오류 로그 대신 `print`로 출력됨 | `Task 4.2` / `P4-R1` | + +#### REV-P4-001 — 신규 ViewModel 로깅 규칙 불일치 + +- **관찰 내용:** `MainHomeRecommendationViewModel.fetchRecommendations()`의 디코딩 `catch`는 toast와 loading 상태를 처리하면서 오류 자체는 `print(error)`로 출력한다. +- **영향:** 신규 코드의 오류가 프로젝트 `ERROR_LOG` 경로와 분리되어 운영·디버그 로그 수집과 검색이 일관되지 않다. 사용자 오류 처리는 유지되므로 심각도는 Low다. +- **근거:** `MainHomeRecommendationViewModel.swift:50-54`; `docs/agent-guides/code-style.md`의 로깅 규칙. +- **재현 또는 검증:** `rg -n 'print\\(' SodaLive/Sources/V2/Main/Home/Recommendation` 실행 시 해당 한 건이 검색되고, 같은 파일의 네트워크 오류는 `ERROR_LOG(error.localizedDescription)`을 사용한다. +- **권장 조치:** toast·loading 분기는 건드리지 않고 `print(error)`만 `ERROR_LOG(error.localizedDescription)`로 교체한다. +- **계획 전환:** `plan-task.md` Phase 4에 `Task 4.2` / `P4-R1`을 추가했다. + +**현재 결론:** `REV-P4-001` 수정 goal 필요 + +#### REV-P4-001 수정 기록 — 2026-07-31 + +- `MainHomeRecommendationViewModel.fetchRecommendations()`의 추천 응답 디코딩 `catch`에서 `print(error)`를 `ERROR_LOG(error.localizedDescription)`로 교체했다. +- 기존 toast, popup, loading 상태 처리는 변경하지 않았다. +- `rg -n "print\(|ERROR_LOG|func fetchRecommendations|catch" "SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift" "SodaLive/Sources/Utils/Log.swift"`로 추천 ViewModel 범위의 직접 `print(` 제거와 `ERROR_LOG` 사용을 확인했다. +- `git diff --check`, `plutil -lint`로 검증했다. +- `SodaLive-dev` Debug 빌드는 같은 DerivedData로 2회 실행했지만 Pods/SPM 및 app resource 처리 중 10분 도구 제한으로 `BUILD INTERRUPTED`되어 성공 증거를 확보하지 못했다. + +**현재 결론:** 코드 수정 완료, 빌드 검증 차단 + +### 4차 최종 검증 — 2026-07-31 + +- `rg -n "print\\(|ERROR_LOG|func fetchRecommendations|catch" SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift SodaLive/Sources/Utils/Log.swift`로 추천 ViewModel에 직접 `print(`가 없고 디코딩 `catch`가 `ERROR_LOG(error.localizedDescription)`를 사용하는 것을 다시 확인했다. +- `git diff --check`, `git diff --cached --check`는 출력 없이 성공했고 `plutil -lint SodaLive.xcodeproj/project.pbxproj`는 `OK`였다. +- `xcodebuild -quiet -workspace SodaLive.xcworkspace -scheme SodaLive-dev -configuration Debug -destination generic/platform=iOS -derivedDataPath /private/tmp/SodaLiveDerivedDataReviewFinal-dev build`는 종료 코드 0으로 통과했다. +- 같은 DerivedData에서 `SodaLive` 운영 스킴도 종료 코드 0으로 통과했다. 출력된 기존 앱·외부 의존성 경고는 이 Low 로깅 항목의 완료 조건을 막지 않는다. +- `plan-task.md`의 `Task 4.2` / `P4-R1`을 완료 처리했다. + +**현재 결론:** `REV-P4-001` 수정 및 검증 완료 diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase5-detail-routing-guards.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase5-detail-routing-guards.md new file mode 100644 index 00000000..bf3d74e7 --- /dev/null +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase5-detail-routing-guards.md @@ -0,0 +1,226 @@ +# Phase 5 추천 상세 진입 라우팅 guard 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 5 / Task 5.1 | +| 기준 commit 또는 working tree | `3ae2dc19d67d0a88bc5043f970e4cfabdfb28b60` + 현재 working tree | +| 리뷰 일자 | 2026-07-30 | +| 리뷰어 | Codex | +| 기준 문서 | PRD 7.3, `plan-task.md` Phase 5 | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +- 추천 상세 callback이 `MainView`의 로그인/본인인증/민감 콘텐츠 guard와 올바른 `AppStep`을 사용하는지 확인한다. +- `MainView`, `MainHomeView`, `MainHomeRecommendationView` callback 체인을 포함한다. + +## 3. 판정 기준 + +- 확정된 상세 타입과 다른 route로 이동하면 High로 판정한다. +- 외부 계약이 필요한 성인 여부는 보류 조건으로 분리하고 임의 구현 여부만 확인한다. + +## 4. 검토한 근거와 실행한 검증 + +- 코드: `MainView.swift:175-197`, `:508-555`, `:707-739`; `HomeTabView.swift:40-71` +- 문서: PRD 7.3의 `characterDetail`, Task 5.1의 실제 상세 이동 기준 +- 환경: macOS 26.0, Xcode 26.0, Swift 6.2 + +| 명령 | 결과 | 핵심 증거 | +|---|---|---| +| `rg "onTapCharacter: handleRecommendationCreatorTap\|characterDetail" ...` | 실패 발견 | 추천 AI callback만 creator handler에 연결 | +| `rg "performRecommendationDetailAction\|pendingAction\|contentViewSettings" MainView.swift` | 성공 | 공통 guard 인프라 존재 | +| `xcodebuild ... -derivedDataPath "/private/tmp/SodaLiveDerivedDataReview20260730" build` | 성공 | 잘못된 타입 연결은 컴파일로 검출되지 않음 | + +## 5. 발견 사항 요약 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | +|---|---|---|---|---| +| `REV-P5-001` | High | 확정 | AI 캐릭터 callback이 크리에이터 상세 handler에 연결됨 | `Task 5.2` / `P5-R1` | + +## 6. 발견 사항 상세 + +### REV-P5-001 — AI 캐릭터 상세 route 오연결 + +- **심각도:** High +- **상태:** 확정 +- **소유 Task:** `Task 5.2` + +**관찰 내용** + +`MainView`는 `onTapCharacter`에 `handleRecommendationCreatorTap`을 전달한다. 따라서 값이 무엇이든 `.creatorDetail(userId:)`로 이동하며 `.characterDetail(characterId:)` 경로가 추천 홈에 없다. + +**근거** + +- 코드: `MainView.swift:186`, `:525-529` +- 정상 선례: `HomeTabView.swift:40-71` +- 실제 route: `AppStep.characterDetail(characterId:)`, `ContentView`의 `CharacterDetailView` + +**영향** + +AI 캐릭터 카드를 탭하면 요청한 캐릭터 상세가 아니라 크리에이터 상세로 이동한다. 캐릭터용 본인인증/민감 콘텐츠 흐름도 적용할 수 없다. + +**권장 조치** + +양수 `characterId`를 받아 기존 추천 guard를 재사용하고 `.characterDetail(characterId:)`를 실행하는 전용 handler를 연결한다. 섹션의 ID 전달은 `REV-P12-001`과 함께 수정한다. + +**판정 기록** + +- 2026-07-30 — callback 소비 지점과 기존 Character route 대조로 확정. + +**보류 조건** + +`HomeLiveItem.isAdult` 부재에 따른 라이브 성인 guard는 PRD에 이미 외부 계약으로 기록되어 있으며 이번 결함과 분리한다. + +## 7. 확정 항목의 plan·goal 전환 + +`plan-task.md` Phase 5에 `Task 5.2` / `P5-R1`을 추가했다. + +## 8. 리뷰 종료 판정 + +| 항목 | 결과 | 근거 | +|---|---|---| +| 범위 확인 | 충족 | callback→guard→AppStep 추적 | +| 후보 판정 | 충족 | `REV-P5-001` 확정 | +| plan 반영 | 충족 | Task 5.2 | +| 보류 조건 기록 | 충족 | 라이브 성인 필드 계약 | +| 검증 기록 | 충족 | 4절 | + +**최종 결론:** 수정 goal 필요 + +## 9. 수정 후 검증 기록 + +- 2026-07-30 — `MainView`에 `handleRecommendationCharacterTap(characterId:)` 추가 후 `onTapCharacter`를 해당 handler에 연결. +- 양수 `characterId`만 통과시키고 기존 `performRecommendationDetailAction(requiresAdultContent: true)` guard를 재사용해 `.characterDetail(characterId:)`로 이동하도록 수정. +- `rg -n "onTapCharacter:|handleRecommendationCharacterTap|characterDetail\(characterId:" "SodaLive/Sources/V2/Main/MainView.swift"` 실행, 연결 확인. +- `git diff --check`, `plutil -lint "SodaLive.xcodeproj/project.pbxproj"`, `SodaLive-dev` Debug 빌드 통과. +- 실제 기기/시뮬레이터 탭 수동 확인은 미실행이며 잔여 확인 사항으로 남김. + +### 2차 통합 재점검 — 2026-07-31 + +- AI 캐릭터 callback이 `handleRecommendationCharacterTap(characterId:)`와 공통 상세 guard를 거쳐 `.characterDetail`로 이어지는 현재 경로를 확인했다. +- `REV-P5-001`은 **수정 완료**로 판정했으며, 추가 확정 발견 사항 없음. + +### 3차 최종 재점검 — 2026-07-31 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | +|---|---|---|---|---| +| `REV-P5-002` | High | 확정 | 본인인증 성공 후 민감 콘텐츠 보기 설정 guard를 건너뜀 | `Task 5.3` / `P5-R2` | + +#### REV-P5-002 — 본인인증 후 민감 콘텐츠 설정 guard 우회 + +- **심각도:** High +- **상태:** 확정 +- **관련 요구사항:** PRD 7.3 추천 탭 상세 진입 guard, Success Criteria의 로그인·본인인증·민감 콘텐츠 설정 순차 통과 +- **소유 Task:** `Task 5.3` / `P5-R2` + +**관찰 내용** + +`performRecommendationDetailAction(requiresAdultContent: true)`은 한국 사용자이고 `auth == false`이면 최종 상세 route closure 자체를 `pendingAction`에 저장하고 설정 검사를 실행하기 전에 반환한다. 이후 `authView`의 인증 성공 callback은 `auth = true`로 바꾼 뒤 저장한 closure를 직접 실행하므로 `UserDefaults.isAdultContentVisible() == false`여도 `contentViewSettings`로 이동하지 않고 캐릭터 상세로 진입한다. + +**근거** + +- 코드: `MainView.swift:371-377`의 인증 성공 후 `pendingAction` 직접 실행 +- 코드: `MainView.swift:714-745`의 본인인증 분기 뒤에 위치한 민감 콘텐츠 설정 검사 +- route 소비: `MainView.swift:539-543`의 AI 캐릭터 상세 action +- 문서: `prd.md` 7.3과 Success Criteria는 상세 route 전에 본인인증과 민감 콘텐츠 보기 설정 guard를 모두 통과하도록 요구한다. + +**재현 또는 검증 절차** + +1. 토큰이 있고 국가 코드가 `KR` 또는 빈 값이며 `auth == false`, `isAdultContentVisible == false`인 상태를 만든다. +2. AI 캐릭터 카드를 터치하고 본인인증을 완료한다. +3. 현재 상태 전이 self-check는 `detail=true settings=false`를 출력한다. +4. 요구 결과는 인증 성공 후 민감 콘텐츠 설정을 다시 검사해 `contentViewSettings`로 이동하고 상세 route는 실행하지 않는 것이다. + +**영향** + +본인인증과 민감 콘텐츠 설정이 모두 필요한 제한된 상태에서 사용자가 비활성화한 민감 콘텐츠 보기 설정을 우회해 AI 캐릭터 상세로 진입한다. 명시적인 콘텐츠 보호 요구사항을 위반하므로 High로 판정한다. + +**권장 조치** + +인증 성공 뒤 저장된 최종 route를 직접 실행하지 말고 기존 성인 콘텐츠 guard의 설정 검사를 다시 통과하게 한다. 별도 guard를 복제하지 않고 `performRecommendationDetailAction(requiresAdultContent: true)` 흐름을 한 번 더 소비하는 최소 수정으로 제한한다. + +**판정 기록** + +- 2026-07-31 — 코드 분기 순서와 최소 Swift 상태 전이 self-check의 `detail=true settings=false` 결과로 확정. + +**계획 전환** + +`plan-task.md` Phase 5에 `Task 5.3` / `P5-R2`를 추가했다. + +#### REV-P5-002 수정 기록 — 2026-07-31 + +- `pendingAction`에 최종 상세 route closure를 직접 저장하지 않고, 인증 성공 후 `performRecommendationDetailAction(requiresAdultContent: true, action:)`를 다시 통과하는 closure를 저장하도록 수정했다. +- 기존 로그인 guard, 비한국 사용자, 이미 본인인증된 사용자, 민감 콘텐츠 설정 활성 사용자 흐름은 변경하지 않았다. +- Swift 상태 전이 self-check로 기존 방식의 `detail=true settings=false` 실패를 재현하고, 수정 방식에서 `settings=false`는 설정 이동·상세 미실행, `settings=true`는 상세 실행이 되는 것을 확인했다. +- `rg -n "pendingAction = \{|performRecommendationDetailAction\(requiresAdultContent: true, action: action\)" "SodaLive/Sources/V2/Main/MainView.swift"`, `git diff --check`, `plutil -lint`로 정적 검증했다. +- `SodaLive-dev` Debug 빌드는 iPhoneOS에서 10분 도구 제한으로 반복 `BUILD INTERRUPTED`, Simulator에서 SPM `Promises` 컴파일 단계 실패로 성공 증거를 확보하지 못했다. +- 실제 기기/시뮬레이터의 Bootpay 본인인증 완료 수동 확인은 이번 환경에서 수행하지 못해 상태 전이 self-check와 정적 검증으로 대체했다. + +**현재 결론:** `REV-P5-001`, `REV-P5-002` 수정 완료 + +### 4차 수정 검증 및 추가 재점검 — 2026-07-31 + +#### 기존 수정 확인 + +- `REV-P5-002` 수정 후 `pendingAction`은 최종 상세 route가 아니라 `performRecommendationDetailAction(requiresAdultContent: true, action:)`를 다시 실행한다. +- 한국 사용자 기준 인증 전후와 민감 콘텐츠 설정 조합 self-check는 `auth-guard-self-check=passed`로 통과했다. +- `git diff --check`, `git diff --cached --check`, `plutil -lint`를 통과했다. +- `SodaLive-dev`와 `SodaLive` Debug `generic/platform=iOS` 빌드는 모두 종료 코드 0으로 통과했다. + +#### 추가 확정 항목 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | +|---|---|---|---|---| +| `REV-P5-003` | High | 확정 | 활성 라이브·AI 대화 경로가 인증 후 민감 콘텐츠 설정 guard를 건너뜀 | `Task 5.4` / `P5-R3` | + +**관찰 내용** + +- `LiveNowAllView.handleLiveNowItemTap`은 한국 사용자의 본인인증이 필요하면 `openLiveDetail` 자체를 `pendingAction`에 저장한다. 인증 성공 callback은 이를 직접 실행하므로 민감 콘텐츠 보기 설정 검사를 다시 수행하지 않는다. +- `CreatorChannelView.handleTalkTap`도 같은 조건에서 `startChat` 자체를 저장하고 인증 성공 후 직접 실행한다. +- 두 화면은 각각 `ContentView`의 `.liveNowAll`, `.creatorDetail` route에서 현재 사용된다. + +**근거** + +- `LiveNowAllView.swift:148-156`, `:165-190` +- `CreatorChannelView.swift:390-398`, `:520-549` +- `ContentView.swift:198-220` +- 최소 상태 전이 결과: 두 경로 모두 `route=true settings=false` + +**영향** + +한국 사용자가 본인인증과 민감 콘텐츠 설정 확인을 모두 필요로 하는 상태에서 성인 라이브 또는 AI 대화로 진입하면, 사용자가 비활성화한 민감 콘텐츠 보기 설정이 적용되지 않는다. 활성 보호 경로의 콘텐츠 정책 우회이므로 High로 판정한다. + +**권장 조치** + +각 화면에서 최종 route를 저장하지 말고 기존 진입 함수를 다시 호출하는 closure를 저장해 인증 성공 후 남은 설정 guard를 통과하게 한다. 별도 공용 guard 계층은 추가하지 않는다. + +**계획 전환** + +`plan-task.md` Phase 5에 `Task 5.4` / `P5-R3`를 추가했다. 현재 앱 route에서 사용되지 않는 레거시 `HomeView`, `HomeTabView`, `ChatTabView`는 이번 Task 범위에서 제외했다. + +#### REV-P5-003 수정 기록 — 2026-07-31 + +- `LiveNowAllView.handleLiveNowItemTap`의 `pendingAction`이 인증 성공 후 `openLiveDetail(roomId:)`를 직접 실행하지 않고 `handleLiveNowItemTap(roomId:isAdult:)`를 다시 호출하도록 수정했다. +- `CreatorChannelView.handleTalkTap`의 `pendingAction`이 인증 성공 후 `startChat(characterId:)`를 직접 실행하지 않고 `handleTalkTap()`을 다시 호출하도록 수정했다. +- 기존 로그인 guard, 국가 분기, 본인인증 dialog, 민감 콘텐츠 설정 이동, 최종 라이브/AI 대화 route는 변경하지 않았다. +- Swift 상태 전이 self-check로 기존 방식의 `route=true settings=false` 실패를 재현하고, 수정 방식에서 `auth=false/설정=false`, `auth=false/설정=true`, `auth=true/설정=false`, `auth=true/설정=true`, 비로그인, 취소 흐름이 기대 상태를 통과하는 것을 확인했다. +- `rg -n "pendingAction = \\{|handleLiveNowItemTap\\(roomId: roomId, isAdult: isAdult\\)|handleTalkTap\\(\\)|openLiveDetail\\(roomId: roomId\\)|startChat\\(characterId: characterId\\)|contentViewSettings|isAdultContentVisible" "SodaLive/Sources/Live/Now/All/LiveNowAllView.swift" "SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift"` 실행, guard 재진입과 기존 route 유지 확인. +- `git diff --check`, `plutil -lint "SodaLive.xcodeproj/project.pbxproj"`, 미완료 Task 검색을 통과했다. +- `SodaLive-dev`, `SodaLive` Debug `generic/platform=iOS` 빌드는 최초 병렬 실행에서 각각 10분 제한으로 `BUILD INTERRUPTED`됐고, 동일 DerivedData 순차 재시도에서 모두 `BUILD SUCCEEDED`를 확인했다. +- 실제 기기/시뮬레이터의 Bootpay 본인인증 완료 수동 확인은 이번 환경에서 수행하지 못해 상태 전이 self-check와 앱 빌드로 대체했다. +- `SodaLive` 빌드의 범위 밖 V2 파일 duplicate Sources warning은 기존 상태로 남아 있으며 이번 Task에서 변경하지 않았다. + +**현재 결론:** `REV-P5-001`, `REV-P5-002`, `REV-P5-003` 수정 완료 + +### 5차 독립 수정 검증 — 2026-07-31 + +- `LiveNowAllView.handleLiveNowItemTap`과 `CreatorChannelView.handleTalkTap`의 인증 후 보류 action이 최종 route가 아닌 동일 진입 함수를 재호출하는 것을 확인했다. +- 두 경로의 인증·민감 콘텐츠 설정 조합과 인증 미완료 상태를 독립 self-check로 다시 실행해 `task-5.4-independent-self-check=passed`를 확인했다. +- `auth == false`와 `isAdultContentVisible`을 함께 사용하는 전체 Swift 화면을 재검색했다. 현재 `ContentView`에서 도달 가능한 `MainView`, `LiveNowAllView`, `CreatorChannelView`는 모두 인증 후 설정 guard를 재진입한다. +- `git diff --check`, `git diff --cached --check`, `plutil -lint`를 통과했다. +- `SodaLive-dev`와 `SodaLive` Debug `generic/platform=iOS` 빌드는 모두 종료 코드 0으로 통과했다. +- 수정 범위와 현재 활성 sibling 경로에서 추가 확정 발견 사항 없음. + +**현재 결론:** `REV-P5-003` 수정 및 검증 완료 diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase6-home-tab-shell.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase6-home-tab-shell.md new file mode 100644 index 00000000..28b48449 --- /dev/null +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase6-home-tab-shell.md @@ -0,0 +1,82 @@ +# Phase 6 홈 탭 shell과 루트 연결 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 6 / Task 6.1~6.3 | +| 기준 commit 또는 working tree | `3ae2dc19d67d0a88bc5043f970e4cfabdfb28b60` + 현재 working tree | +| 리뷰 일자 | 2026-07-30 | +| 리뷰어 | Codex | +| 기준 문서 | PRD 8.2~8.4, `plan-task.md` Phase 6 | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +- `MainHomeView`가 title/tab shell만 소유하고 추천/랭킹/팔로잉 루트를 조합하는지 확인한다. +- `.home` 연결과 기존 하단 탭바 유지 여부를 포함한다. + +## 3. 판정 기준 + +- 탭별 책임 분리, 추천 API 호출 비소유, `MainTabBarView` 비중복을 기준으로 판정한다. + +## 4. 검토한 근거와 실행한 검증 + +- 코드: `MainHomeView.swift:3-111`, `MainView.swift:37-58`, `:175-197` +- 환경: macOS 26.0, Xcode 26.0, Swift 6.2 + +| 명령 | 결과 | 핵심 증거 | +|---|---|---| +| `find SodaLive/Sources/V2/Main/Home -maxdepth 3 -type f` | 성공 | 세 탭 전용 루트 확인 | +| `rg "MainHomeRecommendationView\|MainHomeRankingView\|MainHomeFollowingView\|fetchRecommendations\|MainTabBarView" ...` | 성공 | shell 책임 분리와 탭바 단일 소유 확인 | +| `xcodebuild ... -derivedDataPath "/private/tmp/SodaLiveDerivedDataReview20260730" build` | 성공 | `BUILD SUCCEEDED` | + +## 5. 발견 사항 요약 + +**확정 발견 사항 없음.** + +## 6. 발견 사항 상세 + +확정 발견 사항 없음. AI 캐릭터 callback의 최종 목적지 오류는 Phase 5와 12가 소유한다. + +## 7. 확정 항목의 plan·goal 전환 + +전환 항목 없음. + +## 8. 리뷰 종료 판정 + +| 항목 | 결과 | 근거 | +|---|---|---| +| 범위 확인 | 충족 | 세 루트와 MainView 연결 확인 | +| 후보 판정 | 충족 | 남은 후보 없음 | +| plan 반영 | 해당 없음 | 확정 발견 사항 없음 | +| 검증 기록 | 충족 | 4절 | + +**최종 결론:** 확정 발견 사항 없음 + +## 9. 수정 후 검증 기록 + +### 2차 통합 재점검 — 2026-07-31 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | +|---|---|---|---|---| +| `REV-P6-001` | Medium | 확정 | 홈 상단 탭 제목이 한글 문자열로 고정되어 앱 언어를 따르지 않음 | `Task 6.4` / `P6-R1` | + +#### REV-P6-001 — 홈 상단 탭 제목 현지화 누락 + +- **관찰 내용:** `MainHomeTab.title`이 추천/랭킹/팔로잉을 각각 한글 문자열 리터럴로 반환한다. 같은 의미의 `I18n.MainContentRecommendation.tabTitle`, `I18n.MainContentRanking.tabTitle`, `I18n.HomeFollowing.tabTitle`이 이미 ko/en/ja 값을 제공하지만 사용하지 않는다. +- **영향:** 앱 언어가 영어 또는 일본어여도 홈 상단 세 탭은 한글로 표시된다. +- **근거:** `MainHomeView.swift:101-108`, `I18n.swift`의 기존 세 탭 `tabTitle`. +- **재현:** 앱 언어를 en 또는 ja로 설정하고 홈 화면을 열면 세 탭 제목이 `추천`, `랭킹`, `팔로잉`으로 유지된다. +- **권장 조치:** 새 문자열을 만들지 말고 기존 I18n 키를 직접 재사용한다. +- **계획 전환:** `plan-task.md` Phase 6에 `Task 6.4` / `P6-R1`을 추가했다. + +#### REV-P6-001 수정 기록 — 2026-07-31 + +- `MainHomeTab.title`의 한글 문자열 리터럴을 기존 `I18n.MainContentRecommendation.tabTitle`, `I18n.MainContentRanking.tabTitle`, `I18n.HomeFollowing.tabTitle`로 교체했다. +- `rg -n 'MainContentRecommendation\.tabTitle|MainContentRanking\.tabTitle|HomeFollowing\.tabTitle' SodaLive/Sources/V2/Main/Home/MainHomeView.swift`로 연결을 확인했다. +- `git diff --check`와 `plutil -lint SodaLive.xcodeproj/project.pbxproj`를 통과했다. +- Oracle 리뷰어 게이트에서 차단/중요 findings 없음 판정을 받았다. +- 실제 ko/en/ja 화면 수동 확인은 잔여 확인 사항으로 남긴다. + +**현재 결론:** `REV-P6-001` 수정 완료 diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase7-intermediate-project-build.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase7-intermediate-project-build.md new file mode 100644 index 00000000..4439b40a --- /dev/null +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase7-intermediate-project-build.md @@ -0,0 +1,79 @@ +# Phase 7 프로젝트 포함과 중간 빌드 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 7 / Task 7.1 | +| 기준 commit 또는 working tree | `3ae2dc19d67d0a88bc5043f970e4cfabdfb28b60` + 현재 working tree | +| 리뷰 일자 | 2026-07-30 | +| 리뷰어 | Codex | +| 기준 문서 | `plan-task.md` Phase 7, 빌드/검증 가이드 | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +- Phase 4~6 데이터·라우팅·탭 루트 파일의 Xcode target 포함과 현재 빌드 가능성을 확인한다. + +## 3. 판정 기준 + +- project plist 유효성, app target 실제 Swift compile과 최종 build 성공을 기준으로 판정한다. + +## 4. 검토한 근거와 실행한 검증 + +- 프로젝트: `SodaLive.xcodeproj/project.pbxproj` +- 환경: macOS 26.0, Xcode 26.0, Any iOS Device / Debug + +| 명령 | 결과 | 핵심 증거 | +|---|---|---| +| `plutil -lint SodaLive.xcodeproj/project.pbxproj` | 성공 | `OK` | +| sandbox 내 `xcodebuild -workspace ... -list` | 환경 실패 | CoreSimulator/캐시 접근 제한으로 workspace 판별 실패 | +| sandbox 밖 `xcodebuild ... -derivedDataPath "/private/tmp/SodaLiveDerivedDataReview20260730" build` | 성공 | app target의 `MainHomeView.swift`, 추천 루트 컴파일 및 `BUILD SUCCEEDED` | + +## 5. 발견 사항 요약 + +**확정 발견 사항 없음.** + +## 6. 발견 사항 상세 + +확정 발견 사항 없음. 샌드박스 내 실패는 동일 명령의 샌드박스 밖 성공으로 환경 문제임을 확인했다. + +## 7. 확정 항목의 plan·goal 전환 + +전환 항목 없음. + +## 8. 리뷰 종료 판정 + +| 항목 | 결과 | 근거 | +|---|---|---| +| 프로젝트 포함 확인 | 충족 | 실제 app target compile | +| 후보 판정 | 충족 | 남은 후보 없음 | +| plan 반영 | 해당 없음 | 확정 발견 사항 없음 | +| 검증 기록 | 충족 | 4절 | + +**최종 결론:** 확정 발견 사항 없음 + +## 9. 수정 후 검증 기록 + +### 2차 통합 재점검 — 2026-07-31 + +- Phase 4~6 데이터·루트 파일의 프로젝트 포함과 현재 빌드 기준을 다시 확인했다. +- Phase 7 소유의 추가 확정 발견 사항 없음. 새 Sources 중복은 해당 변경을 만든 Phase 16의 `REV-P16-002`로 전환했다. + +### 3차 통합 재점검 — 2026-07-31 + +- 프로덕션 빌드 경고와 PBX Sources phase를 다시 대조한 결과, Phase 4~6 범위의 `MainHomeRecommendationViewModel.swift`, `MainHomeView.swift`, `MainHomeRecommendationView.swift`, `MainHomeRankingView.swift`, `MainHomeFollowingView.swift`도 `SodaLive` target에 각각 두 번 등록된 사실을 확인했다. +- 같은 패턴이 이후 추천 섹션 파일까지 이어지므로 최종 프로젝트 Gate인 Phase 18의 `REV-P18-001` / `Task 18.3`이 일괄 소유한다. +- 기존 “Phase 16의 신규 중복만 존재” 판정은 불완전했으며, 현재 결론은 `REV-P18-001` 후속 수정 필요다. + +### 4차 최종 재점검 — 2026-07-31 + +- `Task 18.3` 이후 현재 PBX Sources phase를 다시 센 결과 Phase 4~6 범위 파일도 여전히 `SodaLive=2`, `SodaLive-dev=1`이다. +- 격리 DerivedData를 사용한 프로덕션 빌드에서 해당 파일들의 duplicate build warning을 재현했고 빌드는 종료 코드 0으로 완료됐다. +- 잔여 중복은 Phase 18 `REV-P18-002` / `Task 18.4`로 전환했으며, Phase 7 자체의 신규 production 코드 결함은 없다. + +### 5차 최종 재점검 — 2026-07-31 + +- `Task 18.4` 반영 후 `xcodebuild -workspace SodaLive.xcworkspace -list`가 종료 코드 0으로 완료되고 `SodaLive`, `SodaLive-dev` scheme을 확인했다. +- clean DerivedData의 `SodaLive-dev` Debug 전체 빌드가 종료 코드 0으로 완료됐다. +- Phase 4~6 및 홈 추천 범위 대상의 duplicate build warning은 재발하지 않아 Phase 7 추가 확정 발견 사항 없음 판정을 유지한다. diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase8-live-section.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase8-live-section.md new file mode 100644 index 00000000..9f04698f --- /dev/null +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase8-live-section.md @@ -0,0 +1,61 @@ +# Phase 8 현재 라이브 섹션 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 8 / Task 8.1 | +| 기준 commit 또는 working tree | `3ae2dc19d67d0a88bc5043f970e4cfabdfb28b60` + 현재 working tree | +| 리뷰 일자 | 2026-07-30 | +| 리뷰어 | Codex | +| 기준 문서 | PRD 7.1·7.3, `plan-task.md` Phase 8 | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +- 라이브 응답 필드 표시, 빈 상태 숨김과 기존 라이브 입장 흐름 연결을 확인한다. +- 성인 여부 계약 자체는 PRD Open Questions로 제외한다. + +## 3. 판정 기준 + +- `roomId` 전달, `LiveViewModel.enterLiveRoom`, 결제/비밀번호 dialog 재사용 및 빈 배열 미표시를 기준으로 판정한다. + +## 4. 검토한 근거와 실행한 검증 + +- 코드: `MainHomeLiveSection.swift:3-78`, `MainView.swift:508-523`, `:84-104` +- 환경: macOS 26.0, Xcode 26.0, Swift 6.2 + +| 명령 | 결과 | 핵심 증거 | +|---|---|---| +| `rg "MainHomeLiveSection\|roomId\|enterLiveRoom\|LivePaymentDialog\|LiveRoomPasswordDialog" ...` | 성공 | 필드와 기존 입장 흐름 연결 | +| `xcodebuild ... -derivedDataPath "/private/tmp/SodaLiveDerivedDataReview20260730" build` | 성공 | `BUILD SUCCEEDED` | + +## 5. 발견 사항 요약 + +**확정 발견 사항 없음.** + +## 6. 발견 사항 상세 + +확정 발견 사항 없음. `HomeLiveItem.isAdult` 제공 여부는 PRD에 담당 주체 확인이 필요한 외부 조건으로 남아 있다. + +## 7. 확정 항목의 plan·goal 전환 + +전환 항목 없음. + +## 8. 리뷰 종료 판정 + +| 항목 | 결과 | 근거 | +|---|---|---| +| 범위 확인 | 충족 | item→입장 flow 추적 | +| 후보 판정 | 충족 | 코드 결함 후보 없음 | +| 보류 조건 기록 | 충족 | 성인 여부 계약은 PRD Open Questions | +| plan 반영 | 해당 없음 | 확정 발견 사항 없음 | + +**최종 결론:** 확정 발견 사항 없음 + +## 9. 수정 후 검증 기록 + +### 2차 통합 재점검 — 2026-07-31 + +- 현재 라이브 섹션의 빈 상태, 프로필 표시와 room callback 연결을 다시 확인했다. +- 추가 확정 발견 사항 없음. diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase9-banner-section.md b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase9-banner-section.md new file mode 100644 index 00000000..2e3481f9 --- /dev/null +++ b/docs/20260602_메인_홈_추천_UI_API_연동/reviews/phase9-banner-section.md @@ -0,0 +1,122 @@ +# Phase 9 배너 섹션 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 9 / Task 9.1~9.4 | +| 기준 commit 또는 working tree | `3ae2dc19d67d0a88bc5043f970e4cfabdfb28b60` + 현재 working tree | +| 리뷰 일자 | 2026-07-30 | +| 리뷰어 | Codex | +| 기준 문서 | PRD 7.1·8.1, `plan-task.md` Phase 9 | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +- 1:1 카드, 폭/inset/간격/radius, 무한 스크롤, 인디케이터, 응답 매핑과 이동 우선순위를 확인한다. +- 이번 리뷰에서 Figma 실화면 캡처 재비교는 제외하고 기존 검증 기록과 코드 수치를 대조한다. + +## 3. 판정 기준 + +- 확정 수치와 navigation 우선순위, 빈 데이터 숨김, 금지 URL 부재를 기준으로 판정한다. + +## 4. 검토한 근거와 실행한 검증 + +- 코드: `BannerCarousel.swift:13-243`, `MainHomeBannerSection.swift`, `MainView.swift:606-627` +- 환경: macOS 26.0, Xcode 26.0, Swift 6.2 + +| 명령 | 결과 | 핵심 증거 | +|---|---|---| +| `rg "SodaSpacing.s8\|SodaSpacing.s14\|GeometryReader\|currentPage\|visibleItems" BannerCarousel.swift` | 성공 | 간격/radius/인디케이터/순환 구현 확인 | +| `rg "eventItem\|creatorId\|seriesId\|link" MainHomeBannerSection.swift MainView.swift` | 성공 | 첫 non-null 이동 우선순위 확인 | +| 금지 URL 검색 | 성공 | 검색 결과 없음 | +| `xcodebuild ... -derivedDataPath "/private/tmp/SodaLiveDerivedDataReview20260730" build` | 성공 | `BUILD SUCCEEDED` | + +## 5. 발견 사항 요약 + +**확정 발견 사항 없음.** + +## 6. 발견 사항 상세 + +확정 발견 사항 없음. 최신 실기기 시각 회귀는 실행하지 않았으므로 기존 2026-06-29 기록을 근거로 유지한다. + +## 7. 확정 항목의 plan·goal 전환 + +전환 항목 없음. + +## 8. 리뷰 종료 판정 + +| 항목 | 결과 | 근거 | +|---|---|---| +| 코드 범위 확인 | 충족 | carousel과 route 추적 | +| 후보 판정 | 충족 | 남은 코드 후보 없음 | +| 수동 시각 검증 | 기존 증거 사용 | 이번 리뷰에서 재실행 안 함 | +| plan 반영 | 해당 없음 | 확정 발견 사항 없음 | + +**최종 결론:** 확정 발견 사항 없음 + +## 9. 수정 후 검증 기록 + +### 2차 통합 재점검 — 2026-07-31 + +- 배너 응답 매핑, 캐러셀 배치·indicator와 상세 이동 우선순위를 다시 확인했다. +- 추가 확정 발견 사항 없음. + +### 3차 최종 재점검 — 2026-07-31 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | +|---|---|---|---|---| +| `REV-P9-001` | Medium | 확정 | 복제 경계 전환 중 연속 스와이프가 carousel index를 유효 범위 밖으로 보낼 수 있음 | `Task 9.5` / `P9-R1` | + +#### REV-P9-001 — 무한 캐러셀 경계 연속 스와이프 복구 누락 + +- **관찰 내용:** 마지막 실데이터 index `items.count`에서 왼쪽 스와이프하면 `items.count + 1`이 되고 0.35초 뒤 `1`로 reset된다. 그러나 `isSwiping`은 다음 main queue에서 바로 해제되며 gesture도 비활성화되지 않아 reset 전에 같은 방향 스와이프가 끝나면 `items.count + 2`가 된다. `wrapIfNeeded()`는 `0`과 `items.count + 1`만 처리하므로 이 상태를 복구하지 못한다. +- **영향:** HStack의 마지막 복제 item보다 바깥 offset과 잘못된 페이지 번호가 유지되어 배너가 빈 상태로 남을 수 있다. 빠른 경계 연속 입력에서만 발생하므로 Medium으로 판정한다. +- **근거:** `BannerCarousel.swift:140-158`, `:179-198`; `visibleItems`의 유효 index는 `0...items.count + 1`이다. +- **재현 또는 검증:** 마지막 실데이터에서 reset 지연 전에 왼쪽 스와이프 종료를 두 번 적용하면 `displayIndex == items.count + 2`가 되며, 첫 실데이터의 오른쪽 연속 스와이프는 `-1`이 된다. 두 값 모두 현재 wrap 조건에 포함되지 않는다. +- **권장 조치:** 전환 reset이 끝날 때까지 추가 gesture·timer index 변경을 막거나, 모든 범위 밖 index를 한 번의 reset 상태로 정규화하는 최소 guard를 추가한다. 크기·간격·timer·callback은 유지한다. +- **계획 전환:** `plan-task.md` Phase 9에 `Task 9.5` / `P9-R1`을 추가했다. + +**현재 결론:** `REV-P9-001` 수정 goal 필요 + +#### REV-P9-001 수정 기록 — 2026-07-31 + +- `BannerCarousel.wrapIfNeeded()`에서 `displayIndex`를 `0...items.count + 1` 복제 범위로 정규화하도록 보정했다. +- 배너 크기, 간격, 페이지 형식, 5초 자동 스크롤, 탭 callback은 변경하지 않았다. +- Swift 상태 전이 self-check로 기존 조건의 `items.count + 2`와 `-1` 실패를 재현하고, 보정 후 경계 index가 기존 reset 분기로 복구되는 것을 확인했다. +- `rg -n "displayIndex = min\(max\(displayIndex, 0\), items.count \+ 1\)|wrapIfNeeded|currentPage" "SodaLive/Sources/V2/Component/Banner/BannerCarousel.swift"`, `git diff --check`, `plutil -lint`로 검증했다. +- `SodaLive-dev` Debug 빌드는 같은 DerivedData로 2회 실행했지만 Pods/SPM 및 app resource 처리 중 10분 도구 제한으로 `BUILD INTERRUPTED`되어 성공 증거를 확보하지 못했다. +- 실제 기기/시뮬레이터의 빠른 경계 연속 스와이프 시각 확인은 이번 환경에서 수행하지 못해 정적 self-check와 빌드 결과로 대체했다. + +**현재 결론:** 코드 수정 완료, 빌드·수동 시각 검증 차단 + +### 4차 최종 재점검 — 2026-07-31 + +- **상태:** `REV-P9-001` 확정 / 수정 미완료 +- **관찰 내용:** `displayIndex`를 `0...items.count + 1`로 정규화해 같은 방향 연속 입력의 범위 이탈은 막았지만, 경계에서 예약한 `asyncAfter` reset은 실행 시 현재 index를 확인하지 않는다. 마지막 복제 index에서 `1`로 reset을 예약한 뒤 사용자가 반대 방향으로 실제 마지막 index에 돌아와도 이전 reset이 실행되어 현재 index를 `1`로 덮어쓴다. 첫 복제 경계도 대칭으로 같은 문제가 있다. +- **영향:** 빈 배너 지속 문제는 완화됐지만 빠른 양방향 경계 입력에서 사용자가 이동한 페이지와 다른 첫/마지막 페이지로 갑자기 이동한다. `items` 변경 전에 예약된 reset도 새 상태를 덮어쓸 수 있어 Task 9.5의 “안전한 연속 스와이프” 완료 조건을 충족하지 못한다. +- **근거:** `BannerCarousel.swift:179-200`의 두 지연 closure가 조건 없이 `displayIndex`를 대입한다. +- **재현:** 항목 3개 기준 마지막 실제 index `3`에서 왼쪽 입력으로 `4`와 reset을 예약하고, reset 전에 오른쪽 입력으로 `3`에 복귀한다. 상태 전이 self-check 결과는 `beforeDelayedReset=3`, 예약 closure 실행 뒤 `afterDelayedReset=1, expected=3`이다. +- **권장 조치:** 지연 reset 실행 시 `displayIndex`가 예약 당시의 같은 복제 경계에 남아 있는 경우에만 실제 index로 치환한다. 기존 크기·간격·timer·callback과 index 정규화는 유지한다. +- **계획 반영:** 같은 경계 상태를 소유하고 아직 미완료인 `Task 9.5` / `P9-R1`에 반대 방향 입력·데이터 변경 상태 전이와 reset 실행 조건을 추가했다. 중복 회귀 Task는 만들지 않았다. + +#### REV-P9-001 최종 수정 기록 — 2026-07-31 + +- `BannerCarousel.wrapIfNeeded()`에서 지연 reset 예약 시점의 `displayIndex`와 `itemsRevision`을 저장하고, reset 실행 시 두 값이 유지되는 경우에만 실제 index로 치환하도록 수정했다. +- 기존 index 정규화, 배너 크기, 간격, 페이지 형식, 5초 자동 스크롤, 탭 callback은 변경하지 않았다. +- Swift 상태 전이 self-check로 기존 방식의 반대 방향 이동 덮어쓰기 실패를 재현하고, 수정 방식에서 반대 방향 이동과 같은 개수의 `items` 변경은 덮어쓰지 않으며 같은 경계에 남은 경우만 reset되는 것을 확인했다. +- `rg -n "itemsRevision|scheduledIndex|scheduledRevision|wrapIfNeeded|currentPage" "SodaLive/Sources/V2/Component/Banner/BannerCarousel.swift"`, `git diff --check`, `plutil -lint`로 정적 검증했다. +- `SodaLive-dev` Debug 빌드는 iPhoneOS에서 10분 도구 제한으로 반복 `BUILD INTERRUPTED`, Simulator에서 SPM `Promises` 컴파일 단계 실패로 성공 증거를 확보하지 못했다. +- 실제 기기/시뮬레이터의 빠른 경계 연속 스와이프 시각 확인은 이번 환경에서 수행하지 못해 상태 전이 self-check와 정적 검증으로 대체했다. + +**현재 결론:** `REV-P9-001` 수정 완료 + +### 5차 수정 검증 — 2026-07-31 + +- 마지막·첫 복제 경계의 정상 reset, 반대 방향 이동, 같은 개수의 `items` 교체, 반복 같은 방향 입력을 포함한 상태 전이 self-check가 `banner-reset-self-check=passed`로 통과했다. +- 지연 closure는 `displayIndex == scheduledIndex`, `itemsRevision == scheduledRevision`인 경우에만 실제 index를 대입한다. +- `git diff --check`, `git diff --cached --check`, `plutil -lint`를 통과했다. +- `SodaLive-dev`와 `SodaLive` Debug `generic/platform=iOS` 빌드는 모두 종료 코드 0으로 통과했다. +- 수정 범위에서 추가 확정 발견 사항 없음. + +**현재 결론:** `REV-P9-001` 수정 및 검증 완료