fix(main): 현재 라이브 전체보기를 보정한다

This commit is contained in:
Yu Sung
2026-08-03 15:37:27 +09:00
parent 498debbfc7
commit dc99bdf9c9
10 changed files with 172 additions and 160 deletions

View File

@@ -87,7 +87,7 @@ enum AppStep {
onSuccess: (CreateLiveRoomResponse) -> Void onSuccess: (CreateLiveRoomResponse) -> Void
) )
case liveNowAll(onClickParticipant: (Int) -> Void) case liveNowAll
case liveReservationAll( case liveReservationAll(
onClickReservation: (Int) -> Void, onClickReservation: (Int) -> Void,

View File

@@ -216,8 +216,8 @@ struct AppStepLayerView: View {
onSuccess: onSuccess onSuccess: onSuccess
) )
case .liveNowAll(let onClickParticipant): case .liveNowAll:
LiveNowAllView(onClickParticipant: onClickParticipant) LiveNowAllView()
case .liveReservationAll(let onClickReservation, let onClickStart, let onClickCancel, let onTapCreateLive): case .liveReservationAll(let onClickReservation, let onClickStart, let onClickCancel, let onTapCreateLive):
LiveReservationAllView( LiveReservationAllView(

View File

@@ -2367,7 +2367,7 @@ enum I18n {
enum LiveNow { enum LiveNow {
static var allTitle: String { static var allTitle: String {
pick(ko: "지금 라이브 중 전체보기", en: "Live Now - All", ja: "ライブ配信中(全て)") pick(ko: "On Air", en: "On Air", ja: "On Air")
} }
static var sectionTitle: String { static var sectionTitle: String {

View File

@@ -60,13 +60,6 @@ struct LiveView: View {
LazyVStack(spacing: 48) { LazyVStack(spacing: 48) {
SectionLiveNowView( SectionLiveNowView(
items: viewModel.liveNowItems, items: viewModel.liveNowItems,
onClickParticipant: {
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
viewModel.enterLiveRoom(roomId: $0)
} else {
AppState.shared.setAppStep(step: .login)
}
},
onTapItem: { item in onTapItem: { item in
onTapLiveNowItem(item.roomId, item.isAdult) onTapLiveNowItem(item.roomId, item.isAdult)
}, },

View File

@@ -14,117 +14,92 @@ struct LiveNowAllItemView: View {
let itemWidth: CGFloat let itemWidth: CGFloat
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 8) { HStack(alignment: .center, spacing: 14) {
ZStack { KFImage(URL(string: item.creatorProfileImage))
KFImage(URL(string: item.coverImageUrl)) .cancelOnDisappear(true)
.cancelOnDisappear(true) .downsampling(size: CGSize(width: 75, height: 75))
.downsampling( .resizable()
size: CGSize( .scaledToFill()
width: itemWidth, .frame(width: 75, height: 75)
height: itemWidth * 144 / 102 .clipShape(Circle())
)
)
.resizable()
.scaledToFill()
.frame(width: itemWidth, height: itemWidth * 144 / 102, alignment: .center)
.cornerRadius(4.7)
.clipped()
LinearGradient( VStack(alignment: .leading, spacing: 4) {
colors: [Color.black.opacity(0.1), Color.black.opacity(0.8)], HStack(spacing: 8) {
startPoint: .top, liveTag
endPoint: .bottom
)
VStack(alignment: .trailing, spacing: 0) { Text(startTimeText)
HStack(spacing: 0) { .appFont(size: 14, weight: .regular)
HStack(spacing: 1) { .foregroundColor(Color.gray500)
if item.price > 0 {
Image("ic_can_white")
}
Text(item.price > 0 ? "\(item.price)" : I18n.CreateContent.free)
.appFont(size: 12, weight: .medium)
.foregroundColor(Color.white)
}
.padding(.horizontal, 7.3)
.padding(.vertical, 4)
.background(item.price > 0 ? Color.mainRed3 : Color.gray11)
.cornerRadius(13.3)
Spacer()
if item.isPrivateRoom {
Image("ic_lock")
.resizable()
.frame(width: 20, height: 20)
.padding(2.7)
.background(Color.gray33.opacity(0.7))
.clipShape(Circle())
}
}
.padding(.horizontal, 3.3)
.padding(.top, 3.3)
Spacer()
if item.numberOfPeople - item.numberOfParticipate < 3 {
HStack(spacing: 0) {
Spacer()
HStack(spacing: 2) {
Text(I18n.LiveNow.remaining)
.appFont(size: 12, weight: .medium)
.foregroundColor(Color.grayee)
Text("\(item.numberOfPeople - item.numberOfParticipate)")
.appFont(size: 12, weight: .medium)
.foregroundColor(Color.button)
}
.padding(.horizontal, 4)
.padding(.vertical, 3)
.background(Color.gray33.opacity(0.7))
.cornerRadius(13.3)
}
.padding(.horizontal, 3.3)
.padding(.bottom, 3.3)
}
} }
Text(item.title)
.appFont(size: 18, weight: .bold)
.foregroundColor(.white)
.lineLimit(1)
.truncationMode(.tail)
Text(item.creatorNickname)
.appFont(size: 14, weight: .regular)
.foregroundColor(Color.gray500)
.lineLimit(1)
.truncationMode(.tail)
} }
.frame(width: itemWidth, height: itemWidth * 144 / 102) .frame(maxWidth: .infinity, alignment: .leading)
Text("\(item.title)") priceView
.appFont(size: 12, weight: .medium)
.foregroundColor(Color(hex: "eeeeee"))
.lineLimit(2)
.fixedSize(horizontal: false, vertical: true)
if item.tags.count > 0 {
Text("\(item.tags.map { "#\($0)" }.joined(separator: " "))")
.appFont(size: 11, weight: .medium)
.foregroundColor(Color.button)
}
HStack(spacing: 5.3) {
KFImage(URL(string: item.creatorProfileImage))
.cancelOnDisappear(true)
.downsampling(
size: CGSize(
width: 21.3,
height: 21.3
)
)
.resizable()
.scaledToFill()
.frame(width: 21.3, height: 21.3, alignment: .center)
.clipShape(Circle())
Text("\(item.creatorNickname)")
.appFont(size: 10, weight: .medium)
.foregroundColor(Color.gray77)
}
} }
.frame(width: itemWidth) .frame(width: itemWidth, height: 99)
}
private var liveTag: some View {
HStack(spacing: 4) {
Circle()
.fill(Color.red400)
.frame(width: 8, height: 8)
Text("LIVE")
.appFont(size: 12, weight: .regular)
.foregroundColor(.white)
}
.padding(.horizontal, 4)
.padding(.vertical, 2)
.frame(height: 18)
.background(Color.black)
.clipShape(Capsule())
}
@ViewBuilder
private var priceView: some View {
if item.price > 0 {
HStack(spacing: 2) {
Image("ic_bar_cash")
.resizable()
.scaledToFit()
.frame(width: 18, height: 18)
Text("\(item.price)")
.appFont(size: 14, weight: .regular)
.foregroundColor(.white)
.lineLimit(1)
}
.padding(.trailing, 7)
.frame(width: 60, alignment: .trailing)
} else {
Text(I18n.CreateContent.free)
.appFont(size: 14, weight: .regular)
.foregroundColor(.white)
.lineLimit(1)
.padding(.trailing, 7)
.frame(width: 60, alignment: .trailing)
}
}
private var startTimeText: String {
guard let date = DateParser.parse(item.beginDateTimeUtc) else {
return item.beginDateTimeUtc
}
return date.convertDateFormat(dateFormat: "HH:mm")
} }
} }

View File

@@ -13,7 +13,6 @@ import BootpayUI
struct LiveNowAllView: View { struct LiveNowAllView: View {
@StateObject var viewModel = LiveViewModel() @StateObject var viewModel = LiveViewModel()
@StateObject var liveAllViewModel = LiveAllViewModel()
@StateObject var mypageViewModel = MyPageViewModel() @StateObject var mypageViewModel = MyPageViewModel()
let spacing: CGFloat = 16 let spacing: CGFloat = 16
@@ -26,15 +25,6 @@ struct LiveNowAllView: View {
@State private var pendingAction: (() -> Void)? = nil @State private var pendingAction: (() -> Void)? = nil
@State private var payload = Payload() @State private var payload = Payload()
var columns: [GridItem] {
[
GridItem(.flexible(), spacing: spacing, alignment: .top),
GridItem(.flexible(), spacing: spacing, alignment: .top)
]
}
let onClickParticipant: (Int) -> Void
var body: some View { var body: some View {
BaseView(isLoading: $viewModel.isLoading) { BaseView(isLoading: $viewModel.isLoading) {
ZStack { ZStack {
@@ -50,13 +40,13 @@ struct LiveNowAllView: View {
viewModel.getLiveNowList() viewModel.getLiveNowList()
}, },
content: { content: {
let itemWidth = (proxy.size.width - (spacing * 3)) / 2 let itemWidth = proxy.size.width - (spacing * 2)
LazyVGrid(columns: columns, spacing: spacing) { LazyVStack(spacing: spacing) {
ForEach(0..<viewModel.liveNowItems.count, id: \.self) { index in ForEach(0..<viewModel.liveNowItems.count, id: \.self) { index in
let item = viewModel.liveNowItems[index] let item = viewModel.liveNowItems[index]
LiveNowItemView(item: item, itemWidth: itemWidth) LiveNowAllItemView(item: item, itemWidth: itemWidth)
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { .onTapGesture {
handleLiveNowItemTap( handleLiveNowItemTap(
@@ -85,24 +75,6 @@ struct LiveNowAllView: View {
.edgesIgnoringSafeArea(.bottom) .edgesIgnoringSafeArea(.bottom)
} }
if liveAllViewModel.isShowLiveDetail {
LiveDetailView(
roomId: liveAllViewModel.selectedRoomId,
onClickParticipant: {
AppState.shared.isShowPlayer = false
onClickParticipant(liveAllViewModel.selectedRoomId)
},
onClickReservation: {},
onClickStart: {},
onClickCancel: {},
onClickClose: {
withAnimation {
liveAllViewModel.isShowLiveDetail = false
}
}
)
}
if isShowAuthConfirmView { if isShowAuthConfirmView {
SodaDialog( SodaDialog(
title: I18n.Main.Auth.dialogTitle, title: I18n.Main.Auth.dialogTitle,
@@ -120,6 +92,28 @@ struct LiveNowAllView: View {
textAlignment: .center textAlignment: .center
) )
} }
if viewModel.isShowPaymentDialog {
LivePaymentDialog(
title: viewModel.paymentDialogTitle,
desc: viewModel.paymentDialogDesc,
desc2: viewModel.paymentDialogDesc2,
confirmButtonTitle: viewModel.paymentDialogConfirmTitle,
confirmButtonAction: viewModel.paymentDialogConfirmAction,
cancelButtonTitle: viewModel.paymentDialogCancelTitle,
cancelButtonAction: viewModel.hidePopup,
startDateTime: viewModel.liveStartDate,
nowDateTime: viewModel.nowDate
)
}
if viewModel.isShowPasswordDialog {
LiveRoomPasswordDialog(
isShowing: $viewModel.isShowPasswordDialog,
can: viewModel.secretOrPasswordDialogCan,
confirmAction: viewModel.passwordDialogConfirmAction
)
}
} }
} }
.onAppear { .onAppear {
@@ -160,6 +154,7 @@ struct LiveNowAllView: View {
isShowAuthView = false isShowAuthView = false
} }
} }
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
} }
private func handleLiveNowItemTap(roomId: Int, isAdult: Bool) { private func handleLiveNowItemTap(roomId: Int, isAdult: Bool) {
@@ -191,13 +186,11 @@ struct LiveNowAllView: View {
} }
} }
openLiveDetail(roomId: roomId) enterLiveRoom(roomId: roomId)
} }
private func openLiveDetail(roomId: Int) { private func enterLiveRoom(roomId: Int) {
liveAllViewModel.selectedRoomId = roomId AppState.shared.isShowPlayer = false
withAnimation { viewModel.enterLiveRoom(roomId: roomId)
liveAllViewModel.isShowLiveDetail = true
}
} }
} }

View File

@@ -11,7 +11,6 @@ struct SectionLiveNowView: View {
let items: [GetRoomListResponse] let items: [GetRoomListResponse]
let onClickParticipant: (Int) -> Void
let onTapItem: (GetRoomListResponse) -> Void let onTapItem: (GetRoomListResponse) -> Void
let onTapCreateLive: () -> Void let onTapCreateLive: () -> Void
let onClickRefresh: () -> Void let onClickRefresh: () -> Void
@@ -29,7 +28,7 @@ struct SectionLiveNowView: View {
Text(I18n.Common.viewAll) Text(I18n.Common.viewAll)
.appFont(size: 14, weight: .regular) .appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "78909C")) .foregroundColor(Color(hex: "78909C"))
.onTapGesture { AppState.shared.setAppStep(step: .liveNowAll(onClickParticipant: onClickParticipant)) } .onTapGesture { AppState.shared.setAppStep(step: .liveNowAll) }
} }
} }
.padding(.horizontal, 24) .padding(.horizontal, 24)
@@ -95,7 +94,6 @@ struct SectionLiveNowView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
SectionLiveNowView( SectionLiveNowView(
items: [], items: [],
onClickParticipant: { _ in },
onTapItem: { _ in }, onTapItem: { _ in },
onTapCreateLive: {}, onTapCreateLive: {},
onClickRefresh: {} onClickRefresh: {}

View File

@@ -519,7 +519,7 @@ struct MainView: View {
} }
private func handleRecommendationLiveAllTap() { private func handleRecommendationLiveAllTap() {
appState.setAppStep(step: .liveNowAll(onClickParticipant: handleRecommendationLiveTap)) appState.setAppStep(step: .liveNowAll)
} }
private func handleRecommendationCreatorTap(creatorId: Int) { private func handleRecommendationCreatorTap(creatorId: Int) {

View File

@@ -21,6 +21,56 @@
- 기존 `SodaLive/Sources/Home/HomeApi.swift`에는 `/api/v2/home/recommendations`를 추가하지 않는다. - 기존 `SodaLive/Sources/Home/HomeApi.swift`에는 `/api/v2/home/recommendations`를 추가하지 않는다.
- 외부 라이브러리는 추가하지 않는다. - 외부 라이브러리는 추가하지 않는다.
### 2026-08-03 현재 라이브 전체보기 입장 로딩 표시 완료
- 목적: `LiveNowAllView`에서 라이브 항목 탭 후 방 상세 조회/입장 통신 중 멈춘 것처럼 보이지 않도록 기존 로딩 UI 표시
- TDD 예외 사유:
- SwiftUI 화면 내부 입장 흐름이며 앱 테스트 번들 타깃 검색 결과가 없어 production View를 직접 import하는 영구 UI/unit test를 이번 Task에서 추가할 수 없다.
- 수행 내용:
- `LiveNowAllView`가 기존 `LiveViewModel.enterLiveRoom(roomId:)`를 직접 호출하도록 변경
- `BaseView(isLoading: $viewModel.isLoading)`를 통해 방 상세 조회/입장 요청 중 기존 `LoadingView` 표시
- 입장 전 결제 확인 dialog, 비밀방 password dialog, 입장 실패 toast를 `LiveNowAllView`에 연결
- `AppStep.liveNowAll``SectionLiveNowView`에서 더 이상 필요 없는 입장 콜백을 제거
- 검증:
- `rg -n "PBXNativeTarget.*Test|UITests|Tests|xctest|SodaLiveTests|SodaLiveUITests" SodaLive.xcodeproj/project.pbxproj` 실행, 테스트 타깃 없음 확인
- `rg -n "liveNowAll\(|liveNowAll\)|case \.liveNowAll|LiveNowAllView\(|onClickParticipant: onClickParticipant|let onClickParticipant" SodaLive/Sources` 실행
- `rg -n "BaseView\(isLoading: \$viewModel\.isLoading\)|viewModel\.enterLiveRoom\(roomId: roomId\)|LivePaymentDialog\(|LiveRoomPasswordDialog\(|sodaToast\(isPresented: \$viewModel\.isShowPopup" SodaLive/Sources/Live/Now/All/LiveNowAllView.swift` 실행
- `git diff --check` 실행
- `plutil -lint SodaLive.xcodeproj/project.pbxproj` 실행
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` 실행
### 2026-08-03 현재 라이브 전체보기 즉시 입장 변경 완료
- 목적: 추천 탭 `전체` 진입 화면에서 라이브 항목 탭 시 상세 오버레이를 거치지 않고 기존 라이브 입장 흐름으로 바로 연결
- TDD 예외 사유:
- SwiftUI 화면 내부 탭/가드 흐름이며 앱 테스트 번들 타깃이 없어 production View를 직접 import하는 영구 UI/unit test를 이번 Task에서 추가할 수 없다.
- 수행 내용:
- `LiveNowAllView`의 로그인 guard, 한국 사용자 본인인증 guard, 성인 콘텐츠 설정 guard는 유지
- guard 통과 후 `LiveDetailView` 표시 대신 `onClickParticipant(roomId)`를 직접 호출하도록 변경
- 기존 상세 Join 버튼 경로와 동일하게 직접 입장 직전 `AppState.shared.isShowPlayer = false` 처리 유지
- 검증:
- 변경 전 `rg -n "openLiveDetail\(roomId: roomId\)|LiveDetailView\(|liveAllViewModel|onClickParticipant\(liveAllViewModel.selectedRoomId\)" SodaLive/Sources/Live/Now/All/LiveNowAllView.swift` 실행, 상세 오버레이 경유 상태 확인
- 변경 후 `rg -n "LiveDetailView\(|liveAllViewModel|openLiveDetail\(" SodaLive/Sources/Live/Now/All/LiveNowAllView.swift` 실행, 출력 없음으로 상세 오버레이 경유 제거 확인
- 변경 후 `rg -n "enterLiveRoom\(roomId: roomId\)|onClickParticipant\(roomId\)|setAppStep\(step: \.login\)|contentViewSettings|isShowAuthConfirmView|pendingAction = \{ handleLiveNowItemTap" SodaLive/Sources/Live/Now/All/LiveNowAllView.swift` 실행, 즉시 입장 호출과 guard 유지 확인
- `git diff --check` 실행, 출력 없이 성공
- `plutil -lint SodaLive.xcodeproj/project.pbxproj` 실행, `OK`
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` 실행, `BUILD SUCCEEDED`
### 2026-08-03 현재 라이브 전체보기 리스트 UI 보정 완료
- 목적: 추천 탭 `전체` 진입 화면을 Figma `185:4509` 기준의 On Air 리스트 UI로 표시
- 수행 내용:
- `I18n.LiveNow.allTitle``On Air`로 변경
- `LiveNowAllView`의 2열 `LiveNowItemView` 그리드를 단일 `LazyVStack` + `LiveNowAllItemView` 리스트로 변경
- `LiveNowAllItemView``75x75` 원형 프로필, `LIVE` 태그, `HH:mm` 시작 시간, 18pt bold 제목, 14pt regular 크리에이터명, 우측 가격 영역 구조로 보정
- 유료 라이브는 `ic_bar_cash`와 가격, 무료 라이브는 가격/캔 아이콘 대신 `무료`만 표시
- 검증:
- `rg -n "LiveNowItemView\\(|LiveNowAllItemView\\(|DetailNavigationBar\\(title: I18n\\.LiveNow\\.allTitle\\)|static var allTitle|ic_bar_cash|ic_can_white" SodaLive/Sources/Live/Now SodaLive/Sources/I18n/I18n.swift` 실행, 변경 전 전체보기 화면의 2열 카드/기존 아이콘 상태 확인
- `rg -n "pick\\(ko: \\"On Air\\"|LazyVStack|LiveNowAllItemView\\(item: item|Image\\(\\"ic_bar_cash\\"\\)|I18n.CreateContent.free|frame\\(width: 75, height: 75\\)|appFont\\(size: 18, weight: \\.bold\\)|DateParser.parse" SodaLive/Sources/I18n/I18n.swift SodaLive/Sources/Live/Now/All` 실행, 변경 후 제목/리스트/유료/무료/시간/타이포그래피 근거 확인
- `git diff --check` 실행, 출력 없이 성공
- `plutil -lint SodaLive.xcodeproj/project.pbxproj` 실행, `OK`
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` 실행, `BUILD SUCCEEDED`
### 2026-08-03 Phase 8.2 현재 라이브 전체 항목 스타일 보정 완료 ### 2026-08-03 Phase 8.2 현재 라이브 전체 항목 스타일 보정 완료
- 목적: 메인 홈 추천 탭 현재 라이브 섹션 마지막 `전체` 항목을 요청한 텍스트 전용 스타일로 표시 - 목적: 메인 홈 추천 탭 현재 라이브 섹션 마지막 `전체` 항목을 요청한 텍스트 전용 스타일로 표시

View File

@@ -190,6 +190,7 @@ enum class RecommendedActivityType(val code: String) {
- 민감 콘텐츠 보기 설정이 꺼져 있으면 `AppState.shared.setPendingContentSettingsGuideMessage(I18n.Settings.adultContentEnableGuide)`로 안내 문구를 예약하고 `AppState.shared.setAppStep(step: .contentViewSettings)`로 이동한다. - 민감 콘텐츠 보기 설정이 꺼져 있으면 `AppState.shared.setPendingContentSettingsGuideMessage(I18n.Settings.adultContentEnableGuide)`로 안내 문구를 예약하고 `AppState.shared.setAppStep(step: .contentViewSettings)`로 이동한다.
- 위 조건을 통과한 경우에만 `liveDetail`, `creatorDetail`, `contentDetail`, `characterDetail` 등 실제 상세 이동을 실행한다. - 위 조건을 통과한 경우에만 `liveDetail`, `creatorDetail`, `contentDetail`, `characterDetail` 등 실제 상세 이동을 실행한다.
- 추천 탭 현재 라이브 아이템은 `roomId`가 항상 존재하는 값으로 취급하고, 아이템 탭 시 상세 화면을 거치지 않고 기존 `LiveViewModel.enterLiveRoom(roomId:)` 흐름을 호출해 방 상세 조회, 입장 전 결제 확인, 비밀방 비밀번호 dialog, `LiveRoomViewV2` 입장을 재사용한다. - 추천 탭 현재 라이브 아이템은 `roomId`가 항상 존재하는 값으로 취급하고, 아이템 탭 시 상세 화면을 거치지 않고 기존 `LiveViewModel.enterLiveRoom(roomId:)` 흐름을 호출해 방 상세 조회, 입장 전 결제 확인, 비밀방 비밀번호 dialog, `LiveRoomViewV2` 입장을 재사용한다.
- 현재 라이브 전체보기에서 라이브 입장 통신 중에는 기존 `LoadingView`로 진행 중임을 표시한다.
- 최근 활동 크리에이터에서 서버 원본 `activityType == LIVE`이고 `targetId != null`이면 `targetId`를 라이브룸 `roomId`로 사용해 기존 라이브룸 입장 흐름을 실행한다. - 최근 활동 크리에이터에서 서버 원본 `activityType == LIVE`이고 `targetId != null`이면 `targetId`를 라이브룸 `roomId`로 사용해 기존 라이브룸 입장 흐름을 실행한다.
- 최근 활동 크리에이터에서 서버 원본 `activityType == LIVE`이고 `targetId == null`이면 `creatorId`를 사용해 크리에이터 채널로 이동한다. - 최근 활동 크리에이터에서 서버 원본 `activityType == LIVE`이고 `targetId == null`이면 `creatorId`를 사용해 크리에이터 채널로 이동한다.
- 최근 활동 크리에이터의 `creatorId`는 서버에서 항상 유효한 양수로 제공하지만, 앱에서도 `creatorId > 0`을 확인한 뒤에만 크리에이터 채널 이동을 실행한다. 검사를 통과하지 못하면 이동하지 않는다. - 최근 활동 크리에이터의 `creatorId`는 서버에서 항상 유효한 양수로 제공하지만, 앱에서도 `creatorId > 0`을 확인한 뒤에만 크리에이터 채널 이동을 실행한다. 검사를 통과하지 못하면 이동하지 않는다.
@@ -339,6 +340,8 @@ MainHomeView
- 추천 탭에서 `GET /api/v2/home/recommendations`를 호출하고 `success == true` 응답 데이터를 섹션별로 렌더링한다. - 추천 탭에서 `GET /api/v2/home/recommendations`를 호출하고 `success == true` 응답 데이터를 섹션별로 렌더링한다.
- 응답 배열이 비어 있는 섹션은 화면에 표시하지 않는다. - 응답 배열이 비어 있는 섹션은 화면에 표시하지 않는다.
- 현재 라이브 섹션의 마지막 `전체` 항목은 배경 없이 medium 14 텍스트, `soda400` 색상, 세로 가운데 정렬로 표시된다. - 현재 라이브 섹션의 마지막 `전체` 항목은 배경 없이 medium 14 텍스트, `soda400` 색상, 세로 가운데 정렬로 표시된다.
- 현재 라이브 `전체` 진입 화면의 상단 제목은 `On Air`로 표시하고, 라이브 아이템은 Figma `185:4509`의 리스트형 구조를 따른다.
- 현재 라이브 `전체` 진입 화면의 유료 라이브는 `ic_bar_cash`와 가격을 표시하고, 무료 라이브는 가격과 캔 아이콘 대신 `무료`만 표시한다.
- `추천 필모그래피`, `또 다른 모습`은 코드와 화면에 포함하지 않는다. - `추천 필모그래피`, `또 다른 모습`은 코드와 화면에 포함하지 않는다.
- `LIVE`, `LIVE_REPLAY`, `AUDIO`, `COMMUNITY` 활동 타입이 I18n 문구로 표시된다. - `LIVE`, `LIVE_REPLAY`, `AUDIO`, `COMMUNITY` 활동 타입이 I18n 문구로 표시된다.
- `FeedCommunityView`에서 키워드 영역이 제거되고, 이미지/유료 미구매/유료 구매함 또는 무료 상태가 조건에 맞게 표시된다. - `FeedCommunityView`에서 키워드 영역이 제거되고, 이미지/유료 미구매/유료 구매함 또는 무료 상태가 조건에 맞게 표시된다.