From ca565a2b5f0ba3fcb01e8ca39e101bbfb5df58c4 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Thu, 5 Mar 2026 10:55:55 +0900 Subject: [PATCH] =?UTF-8?q?feat(live):=20=EB=9D=BC=EC=9D=B4=EB=B8=8C?= =?UTF-8?q?=EB=A3=B8=20=EA=B2=8C=EC=8A=A4=ED=8A=B8=20=EC=83=81=EB=8B=A8?= =?UTF-8?q?=EC=97=90=20=ED=8C=94=EB=A1=9C=EC=9A=B0=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=EA=B3=BC=20=EC=95=8C=EB=A6=BC=20=EC=98=B5=EC=85=98=EC=9D=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Live/Room/LiveRoomViewModel.swift | 9 +++- .../View/LiveRoomInfoGuestView.swift | 11 +++++ .../Sources/Live/Room/V2/LiveRoomViewV2.swift | 46 +++++++++++++++++++ docs/20260305_라이브룸팔로우버튼추가.md | 20 ++++++++ 4 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 docs/20260305_라이브룸팔로우버튼추가.md diff --git a/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift b/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift index 2660665..19e0f2a 100644 --- a/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift +++ b/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift @@ -1278,7 +1278,12 @@ final class LiveRoomViewModel: NSObject, ObservableObject { .store(in: &subscription) } - func creatorFollow(creatorId: Int? = nil, isGetUserProfile: Bool = false) { + func creatorFollow( + creatorId: Int? = nil, + follow: Bool = true, + notify: Bool = true, + isGetUserProfile: Bool = false + ) { var userId = 0 if let creatorId = creatorId { @@ -1290,7 +1295,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject { if userId > 0 { isLoading = true - userRepository.creatorFollow(creatorId: userId) + userRepository.creatorFollow(creatorId: userId, follow: follow, notify: notify) .sink { result in switch result { case .finished: diff --git a/SodaLive/Sources/Live/Room/V2/Component/View/LiveRoomInfoGuestView.swift b/SodaLive/Sources/Live/Room/V2/Component/View/LiveRoomInfoGuestView.swift index 5294f28..d013e13 100644 --- a/SodaLive/Sources/Live/Room/V2/Component/View/LiveRoomInfoGuestView.swift +++ b/SodaLive/Sources/Live/Room/V2/Component/View/LiveRoomInfoGuestView.swift @@ -24,6 +24,7 @@ struct LiveRoomInfoGuestView: View { let creatorId: Int let creatorNickname: String let creatorProfileUrl: String + let followButtonType: FollowButtonImageType let speakerList: [LiveRoomMember] let muteSpeakerList: [UInt] let activeSpeakerList: [UInt] @@ -38,6 +39,7 @@ struct LiveRoomInfoGuestView: View { let onClickMenuPan: () -> Void let onClickTotalHeart: () -> Void let onClickTotalDonation: () -> Void + let onClickFollow: () -> Void let onClickChangeListener: () -> Void let onClickToggleV2VCaption: () -> Void let onClickToggleSignature: () -> Void @@ -210,6 +212,13 @@ struct LiveRoomInfoGuestView: View { .stroke(Color.graybb, lineWidth: 1) ) .onTapGesture { onClickTotalDonation() } + + if creatorId != UserDefaults.int(forKey: .userId) { + let asset = FollowButtonImageAsset(type: followButtonType) + asset.imageView(defaultSize: CGSize(width: 83.3, height: 26.7)) + .contentShape(Rectangle()) + .onTapGesture { onClickFollow() } + } } } @@ -245,6 +254,7 @@ struct LiveRoomInfoGuestView_Previews: PreviewProvider { creatorId: 1, creatorNickname: "도화", creatorProfileUrl: "https://cf.sodalive.net/profile/26/26-profile-ddf78b4d-0300-4c50-9c84-5d8a95fd5fe2-4892-1705256364320", + followButtonType: .follow, speakerList: [ LiveRoomMember( id: 1, @@ -276,6 +286,7 @@ struct LiveRoomInfoGuestView_Previews: PreviewProvider { onClickMenuPan: {}, onClickTotalHeart: {}, onClickTotalDonation: {}, + onClickFollow: {}, onClickChangeListener: {}, onClickToggleV2VCaption: {}, onClickToggleSignature: {} diff --git a/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift b/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift index a7bed2e..154ccf2 100644 --- a/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift +++ b/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift @@ -24,6 +24,8 @@ struct LiveRoomViewV2: View { @State private var showWaterHeart: Bool = false @State private var waterProgress: CGFloat = 0 @State private var wavePhase: CGFloat = 0 + @State private var isShowFollowNotifyDialog: Bool = false + @State private var guestFollowButtonTypeOverride: FollowButtonImageType? = nil let heartWaveTimer = Timer.publish(every: 1/60, on: .main, in: .common).autoconnect() var body: some View { @@ -101,6 +103,7 @@ struct LiveRoomViewV2: View { creatorId: liveRoomInfo.creatorId, creatorNickname: liveRoomInfo.creatorNickname, creatorProfileUrl: liveRoomInfo.creatorProfileUrl, + followButtonType: guestFollowButtonType(liveRoomInfo: liveRoomInfo), speakerList: liveRoomInfo.speakerList, muteSpeakerList: viewModel.muteSpeakers, activeSpeakerList: viewModel.activeSpeakers, @@ -131,6 +134,16 @@ struct LiveRoomViewV2: View { onClickTotalDonation: { viewModel.isShowDonationRankingPopup = true }, + onClickFollow: { + let buttonType = guestFollowButtonType(liveRoomInfo: liveRoomInfo) + + if buttonType == .follow { + guestFollowButtonTypeOverride = .following + viewModel.creatorFollow(follow: true, notify: true) + } else { + isShowFollowNotifyDialog = true + } + }, onClickChangeListener: { viewModel.setListener() }, @@ -735,6 +748,26 @@ struct LiveRoomViewV2: View { } } } + + if isShowFollowNotifyDialog, + let liveRoomInfo = viewModel.liveRoomInfo, + liveRoomInfo.creatorId != UserDefaults.int(forKey: .userId) { + CreatorFollowNotifyDialog( + isShowing: $isShowFollowNotifyDialog, + onClickNotifyAll: { + guestFollowButtonTypeOverride = .following + viewModel.creatorFollow(follow: true, notify: true) + }, + onClickNotifyNone: { + guestFollowButtonTypeOverride = .followingNoAlarm + viewModel.creatorFollow(follow: true, notify: false) + }, + onClickUnFollow: { + guestFollowButtonTypeOverride = .follow + viewModel.creatorFollow(follow: false, notify: false) + } + ) + } } if viewModel.isShowRouletteSettings { @@ -890,6 +923,11 @@ struct LiveRoomViewV2: View { .sheet(isPresented: $viewModel.isShowDonationMessagePopup) { LiveRoomDonationMessageDialog(viewModel: viewModel, isShowing: $viewModel.isShowDonationMessagePopup) } + .onChange(of: viewModel.liveRoomInfo?.isFollowing) { isFollowing in + if isFollowing == false { + guestFollowButtonTypeOverride = nil + } + } } private func estimatedHeight(for text: String, width: CGFloat) -> CGFloat { @@ -941,6 +979,14 @@ struct LiveRoomViewV2: View { } private extension LiveRoomViewV2 { + func guestFollowButtonType(liveRoomInfo: GetRoomInfoResponse) -> FollowButtonImageType { + if liveRoomInfo.isFollowing { + return guestFollowButtonTypeOverride ?? .following + } + + return .follow + } + var isV2VCaptionVisible: Bool { viewModel.isV2VCaptionOn && !viewModel.v2vCaptionText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty diff --git a/docs/20260305_라이브룸팔로우버튼추가.md b/docs/20260305_라이브룸팔로우버튼추가.md new file mode 100644 index 0000000..ca82c8c --- /dev/null +++ b/docs/20260305_라이브룸팔로우버튼추가.md @@ -0,0 +1,20 @@ +# 20260305 라이브룸 팔로우 버튼 추가 + +## 구현 체크리스트 +- [x] LiveRoom 상단 참여자 수 영역 구조 확인 +- [x] UserProfileView의 팔로우 버튼 이미지/다이얼로그 패턴 확인 +- [x] LiveRoomViewV2에 팔로우/팔로잉 버튼 노출 조건 추가 (방장 본인 제외) +- [x] 팔로잉 상태에서 언팔로우 다이얼로그 노출 및 액션 연결 +- [x] 관련 파일 진단/빌드 검증 수행 + +## 검증 기록 +- 무엇/왜/어떻게: 라이브룸 V2 게스트 상단에 `FollowButtonImageAsset` 기반 팔로우/팔로잉 이미지 버튼을 추가하고, 팔로잉 상태 탭 시 `CreatorFollowNotifyDialog`를 통해 알림 전체/알림 끔/언팔로우를 선택하도록 연결했다. 방장 본인은 버튼이 보이지 않도록 조건을 유지했다. +- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build` +- 결과: `** BUILD SUCCEEDED **` +- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test` +- 결과: `Scheme SodaLive is not currently configured for the test action.` (현재 스킴 테스트 액션 미구성으로 실행 불가) +- 실행 명령: `lsp_diagnostics` (수정 파일 3개) +- 결과: SourceKit 환경에서 외부 모듈(`Kingfisher`, `Moya`) 해석 불가로 오탐 오류 다수 발생. 실제 컴파일은 `xcodebuild` 성공 기준으로 검증. +- 무엇/왜/어떻게: 후속 이슈로 "알림 없음" 선택 후 버튼 이미지가 `followingNoAlarm`으로 유지되지 않는 문제를 수정했다. `GetRoomInfoResponse`에 notify 상태 필드가 없어, 게스트 상단 버튼에 로컬 override 상태를 두고 `notify=false` 선택 시 `FollowButtonImageType.followingNoAlarm`을 우선 표시하도록 반영했다. +- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build` +- 결과: `** BUILD SUCCEEDED **`