feat(home): 현재 라이브 섹션을 연결한다
This commit is contained in:
@@ -213,6 +213,9 @@
|
|||||||
401A3FB12FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB52FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift */; };
|
401A3FB12FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB52FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift */; };
|
||||||
401A3FB22FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB62FEEAFA000A5BBA3 /* MainHomeRankingView.swift */; };
|
401A3FB22FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB62FEEAFA000A5BBA3 /* MainHomeRankingView.swift */; };
|
||||||
401A3FB32FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB72FEEAFA000A5BBA3 /* MainHomeFollowingView.swift */; };
|
401A3FB32FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401A3FB72FEEAFA000A5BBA3 /* MainHomeFollowingView.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 */; };
|
||||||
40235DED2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40235DEC2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift */; };
|
40235DED2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40235DEC2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift */; };
|
||||||
40235DEE2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40235DEC2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift */; };
|
40235DEE2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40235DEC2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift */; };
|
||||||
40235DF02E37983D00B09A87 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40235DEF2E37983D00B09A87 /* PersistenceController.swift */; };
|
40235DF02E37983D00B09A87 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40235DEF2E37983D00B09A87 /* PersistenceController.swift */; };
|
||||||
@@ -1726,6 +1729,7 @@
|
|||||||
401A3FB52FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeRecommendationView.swift; sourceTree = "<group>"; };
|
401A3FB52FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeRecommendationView.swift; sourceTree = "<group>"; };
|
||||||
401A3FB62FEEAFA000A5BBA3 /* MainHomeRankingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeRankingView.swift; sourceTree = "<group>"; };
|
401A3FB62FEEAFA000A5BBA3 /* MainHomeRankingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeRankingView.swift; sourceTree = "<group>"; };
|
||||||
401A3FB72FEEAFA000A5BBA3 /* MainHomeFollowingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingView.swift; sourceTree = "<group>"; };
|
401A3FB72FEEAFA000A5BBA3 /* MainHomeFollowingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingView.swift; sourceTree = "<group>"; };
|
||||||
|
401A3FC12FEEB00100A5BBA3 /* MainHomeLiveSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeLiveSection.swift; sourceTree = "<group>"; };
|
||||||
40235DEC2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RecentContent+CoreDataClass.swift"; sourceTree = "<group>"; };
|
40235DEC2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RecentContent+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||||
40235DEF2E37983D00B09A87 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
|
40235DEF2E37983D00B09A87 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
|
||||||
40235DF22E37987300B09A87 /* RecentContentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentContentService.swift; sourceTree = "<group>"; };
|
40235DF22E37987300B09A87 /* RecentContentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentContentService.swift; sourceTree = "<group>"; };
|
||||||
@@ -2694,9 +2698,18 @@
|
|||||||
path = Repository;
|
path = Repository;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
401A3FC02FEEB00100A5BBA3 /* Components */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
401A3FC12FEEB00100A5BBA3 /* MainHomeLiveSection.swift */,
|
||||||
|
);
|
||||||
|
path = Components;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
401A3F9D2FEEAFA000A5BBA3 /* Recommendation */ = {
|
401A3F9D2FEEAFA000A5BBA3 /* Recommendation */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
401A3FC02FEEB00100A5BBA3 /* Components */,
|
||||||
401A3F992FEEAFA000A5BBA3 /* Models */,
|
401A3F992FEEAFA000A5BBA3 /* Models */,
|
||||||
401A3F9C2FEEAFA000A5BBA3 /* Repository */,
|
401A3F9C2FEEAFA000A5BBA3 /* Repository */,
|
||||||
401A3FB52FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift */,
|
401A3FB52FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift */,
|
||||||
@@ -5314,6 +5327,7 @@
|
|||||||
401A3FBA2FEEAFA000A5BBA3 /* MainHomeRecommendationViewModel.swift in Sources */,
|
401A3FBA2FEEAFA000A5BBA3 /* MainHomeRecommendationViewModel.swift in Sources */,
|
||||||
401A3FA82FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */,
|
401A3FA82FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */,
|
||||||
401A3FA92FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */,
|
401A3FA92FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */,
|
||||||
|
401A3FC22FEEB00100A5BBA3 /* MainHomeLiveSection.swift in Sources */,
|
||||||
401A3FAA2FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */,
|
401A3FAA2FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */,
|
||||||
401A3FAB2FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */,
|
401A3FAB2FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */,
|
||||||
409143302A85652F00EEBFB1 /* CanChargeView.swift in Sources */,
|
409143302A85652F00EEBFB1 /* CanChargeView.swift in Sources */,
|
||||||
@@ -6060,6 +6074,7 @@
|
|||||||
BA2B76FDCAF81D15E2D4E258 /* MainHomeRecommendationViewModel.swift in Sources */,
|
BA2B76FDCAF81D15E2D4E258 /* MainHomeRecommendationViewModel.swift in Sources */,
|
||||||
401A3FAC2FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */,
|
401A3FAC2FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */,
|
||||||
401A3FAD2FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */,
|
401A3FAD2FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */,
|
||||||
|
401A3FC32FEEB00100A5BBA3 /* MainHomeLiveSection.swift in Sources */,
|
||||||
401A3FAE2FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */,
|
401A3FAE2FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */,
|
||||||
401A3FAF2FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */,
|
401A3FAF2FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */,
|
||||||
B3AF650899B8FC84E0C0CDA0 /* MainPlaceholderTabView.swift in Sources */,
|
B3AF650899B8FC84E0C0CDA0 /* MainPlaceholderTabView.swift in Sources */,
|
||||||
@@ -6833,6 +6848,7 @@
|
|||||||
A4EB669DFFFB64681E045EF3 /* MainHomeRecommendationViewModel.swift in Sources */,
|
A4EB669DFFFB64681E045EF3 /* MainHomeRecommendationViewModel.swift in Sources */,
|
||||||
401A3FB02FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */,
|
401A3FB02FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */,
|
||||||
401A3FB12FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */,
|
401A3FB12FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */,
|
||||||
|
401A3FC42FEEB00100A5BBA3 /* MainHomeLiveSection.swift in Sources */,
|
||||||
401A3FB22FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */,
|
401A3FB22FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */,
|
||||||
401A3FB32FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */,
|
401A3FB32FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */,
|
||||||
ACD71056E97E13E48941ABBB /* MainPlaceholderTabView.swift in Sources */,
|
ACD71056E97E13E48941ABBB /* MainPlaceholderTabView.swift in Sources */,
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct MainHomeLiveSection: View {
|
||||||
|
let items: [HomeLiveItem]
|
||||||
|
let onTapLive: (Int) -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if !items.isEmpty {
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
LazyHStack(alignment: .top, spacing: SodaSpacing.s12) {
|
||||||
|
ForEach(items, id: \.self) { item in
|
||||||
|
MainHomeLiveItemView(item: item, onTapLive: onTapLive)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, SodaSpacing.s20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct MainHomeLiveItemView: View {
|
||||||
|
let item: HomeLiveItem
|
||||||
|
let onTapLive: (Int) -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button {
|
||||||
|
onTapLive(item.roomId)
|
||||||
|
} label: {
|
||||||
|
VStack(alignment: .center, spacing: SodaSpacing.s6) {
|
||||||
|
ZStack(alignment: .bottom) {
|
||||||
|
DownsampledKFImage(url: URL(string: item.creatorProfileImage), size: CGSize(width: 62, height: 62))
|
||||||
|
.background(Color.gray800)
|
||||||
|
.clipShape(Circle())
|
||||||
|
.padding(SodaSpacing.s4)
|
||||||
|
.overlay {
|
||||||
|
Circle()
|
||||||
|
.strokeBorder(Color.button, lineWidth: 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
Image("img_live")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 50, height: 18)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(item.creatorNickname)
|
||||||
|
.appFont(.body4)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
}
|
||||||
|
.frame(width: 70, height: 102, alignment: .top)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MainHomeLiveSection_Previews: PreviewProvider {
|
||||||
|
private static let previewImageUrl = "https://placehold.co/500"
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
MainHomeLiveItemView(
|
||||||
|
item: HomeLiveItem(roomId: 1, creatorNickname: "크리에이...", creatorProfileImage: previewImageUrl),
|
||||||
|
onTapLive: { _ in }
|
||||||
|
)
|
||||||
|
.background(Color.black)
|
||||||
|
.previewLayout(.fixed(width: 70, height: 102))
|
||||||
|
.previewDisplayName("Live Item")
|
||||||
|
|
||||||
|
MainHomeLiveSection(
|
||||||
|
items: [
|
||||||
|
HomeLiveItem(roomId: 1, creatorNickname: "설린", creatorProfileImage: previewImageUrl),
|
||||||
|
HomeLiveItem(roomId: 2, creatorNickname: "크리에이터", creatorProfileImage: previewImageUrl)
|
||||||
|
],
|
||||||
|
onTapLive: { _ in }
|
||||||
|
)
|
||||||
|
.padding(.vertical, SodaSpacing.s20)
|
||||||
|
.background(Color.black)
|
||||||
|
.previewLayout(.sizeThatFits)
|
||||||
|
.previewDisplayName("Live Section")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,16 @@ struct MainHomeRecommendationView: View {
|
|||||||
if viewModel.isLoading && viewModel.recommendations == nil {
|
if viewModel.isLoading && viewModel.recommendations == nil {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||||
|
} else {
|
||||||
|
ScrollView(showsIndicators: false) {
|
||||||
|
VStack(alignment: .leading, spacing: SodaSpacing.s24) {
|
||||||
|
MainHomeLiveSection(
|
||||||
|
items: viewModel.recommendations?.lives ?? [],
|
||||||
|
onTapLive: onTapLive
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.padding(.vertical, SodaSpacing.s20)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ struct HomeRecommendationResponse: Decodable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct HomeLiveItem: Decodable, Hashable {
|
struct HomeLiveItem: Decodable, Hashable {
|
||||||
let roomId: Int?
|
let roomId: Int
|
||||||
let creatorNickname: String?
|
let creatorNickname: String
|
||||||
let creatorProfileImage: String?
|
let creatorProfileImage: String
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RecommendationBannerResponse: Decodable, Hashable {
|
struct RecommendationBannerResponse: Decodable, Hashable {
|
||||||
@@ -30,56 +30,56 @@ struct RecommendationBannerResponse: Decodable, Hashable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct HomeActiveCreatorItem: Decodable, Hashable {
|
struct HomeActiveCreatorItem: Decodable, Hashable {
|
||||||
let creatorNickname: String?
|
let creatorNickname: String
|
||||||
let creatorProfileImage: String?
|
let creatorProfileImage: String
|
||||||
let activityType: RecommendedActivityType?
|
let activityType: RecommendedActivityType
|
||||||
let activityAt: String?
|
let activityAt: String
|
||||||
let targetId: Int?
|
let targetId: Int?
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HomeCreatorItem: Decodable, Hashable {
|
struct HomeCreatorItem: Decodable, Hashable {
|
||||||
let creatorId: Int?
|
let creatorId: Int
|
||||||
let creatorNickname: String?
|
let creatorNickname: String
|
||||||
let creatorProfileImage: String?
|
let creatorProfileImage: String
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HomeFirstAudioContentItem: Decodable, Hashable {
|
struct HomeFirstAudioContentItem: Decodable, Hashable {
|
||||||
let contentId: Int?
|
let contentId: Int
|
||||||
let creatorId: Int?
|
let creatorId: Int
|
||||||
let creatorNickname: String?
|
let creatorNickname: String
|
||||||
let creatorProfileImage: String?
|
let creatorProfileImage: String
|
||||||
let title: String?
|
let title: String
|
||||||
let price: Int?
|
let price: Int
|
||||||
let coverImage: String?
|
let coverImage: String?
|
||||||
let isPointAvailable: Bool?
|
let isPointAvailable: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HomeAiCharacterItem: Decodable, Hashable {
|
struct HomeAiCharacterItem: Decodable, Hashable {
|
||||||
let characterId: Int?
|
let characterId: Int
|
||||||
let creatorId: Int?
|
let creatorId: Int
|
||||||
let name: String?
|
let name: String
|
||||||
let description: String?
|
let description: String
|
||||||
let profileImage: String?
|
let profileImage: String?
|
||||||
let totalChatCount: Int?
|
let totalChatCount: Int
|
||||||
let originalWorkTitle: String?
|
let originalWorkTitle: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HomeGenreCreatorGroupItem: Decodable, Hashable {
|
struct HomeGenreCreatorGroupItem: Decodable, Hashable {
|
||||||
let genreName: String?
|
let genreName: String
|
||||||
let creators: [HomeCreatorItem]?
|
let creators: [HomeCreatorItem]
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HomePopularCommunityPostItem: Decodable, Hashable {
|
struct HomePopularCommunityPostItem: Decodable, Hashable {
|
||||||
let postId: Int?
|
let postId: Int
|
||||||
let creatorId: Int?
|
let creatorId: Int
|
||||||
let creatorNickname: String?
|
let creatorNickname: String
|
||||||
let creatorProfileImage: String?
|
let creatorProfileImage: String?
|
||||||
let content: String?
|
let content: String
|
||||||
let imageUrl: String?
|
let imageUrl: String?
|
||||||
let audioUrl: String?
|
let audioUrl: String?
|
||||||
let price: Int?
|
let price: Int
|
||||||
let existOrdered: Bool?
|
let existOrdered: Bool
|
||||||
let likeCount: Int?
|
let likeCount: Int
|
||||||
let commentCount: Int?
|
let commentCount: Int
|
||||||
let createdAt: String?
|
let createdAt: String
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -440,7 +440,13 @@ struct MainView: View {
|
|||||||
private func handleRecommendationLiveTap(roomId: Int) {
|
private func handleRecommendationLiveTap(roomId: Int) {
|
||||||
guard roomId > 0 else { return }
|
guard roomId > 0 else { return }
|
||||||
performRecommendationDetailAction {
|
performRecommendationDetailAction {
|
||||||
openLiveDetail(roomId: roomId)
|
handleExternalNavigationRequest(
|
||||||
|
value: roomId,
|
||||||
|
navigationAction: {
|
||||||
|
liveViewModel.enterLiveRoom(roomId: roomId)
|
||||||
|
},
|
||||||
|
cancelAction: {}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -227,16 +227,18 @@
|
|||||||
|
|
||||||
### Phase 8: 현재 라이브 섹션 UI
|
### Phase 8: 현재 라이브 섹션 UI
|
||||||
|
|
||||||
- [ ] **Task 8.1: 현재 라이브 섹션 작성/보완**
|
- [x] **Task 8.1: 현재 라이브 섹션 작성/보완**
|
||||||
- 대상 파일:
|
- 대상 파일:
|
||||||
- 생성/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeLiveSection.swift`
|
- 생성/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeLiveSection.swift`
|
||||||
- 작업 내용:
|
- 작업 내용:
|
||||||
- `HomeLiveItem.roomId`, `creatorNickname`, `creatorProfileImage`로 라이브 아이템을 표시한다.
|
- `HomeLiveItem.roomId`, `creatorNickname`, `creatorProfileImage`로 라이브 아이템을 표시한다.
|
||||||
|
- `roomId`는 응답 스펙상 항상 존재하는 값으로 취급한다.
|
||||||
|
- 섹션 제목은 표시하지 않는다.
|
||||||
- 데이터가 비어 있으면 섹션 제목과 컨테이너를 표시하지 않는다.
|
- 데이터가 비어 있으면 섹션 제목과 컨테이너를 표시하지 않는다.
|
||||||
- 아이템 터치 이동은 라우팅 규칙이 명확한 경우에만 연결한다.
|
- 아이템 터치 시 기존 `LiveViewModel.enterLiveRoom(roomId:)` 흐름을 재사용해 로그인 guard 이후 입장 전 결제 확인, 비밀방 비밀번호 dialog, `LiveRoomViewV2` 입장을 처리한다.
|
||||||
- 검증 기준:
|
- 검증 기준:
|
||||||
- 실행 명령: `rg "MainHomeLiveSection|roomId|creatorNickname|creatorProfileImage" SodaLive/Sources/V2/Main/Home/Recommendation`
|
- 실행 명령: `rg "MainHomeLiveSection|roomId|creatorNickname|creatorProfileImage|enterLiveRoom|LivePaymentDialog|LiveRoomPasswordDialog" SodaLive/Sources/V2/Main/Home/Recommendation SodaLive/Sources/V2/Main/MainView.swift`
|
||||||
- 기대 결과: 라이브 섹션이 신규 응답 필드를 기준으로 구성된다.
|
- 기대 결과: 라이브 섹션이 신규 응답 필드를 기준으로 구성되고, 추천 라이브 탭이 기존 라이브 입장 흐름을 재사용한다.
|
||||||
|
|
||||||
### Phase 9: 배너 섹션 UI
|
### Phase 9: 배너 섹션 UI
|
||||||
|
|
||||||
@@ -442,6 +444,38 @@
|
|||||||
|
|
||||||
## 검증 기록
|
## 검증 기록
|
||||||
|
|
||||||
|
### 2026-06-29 Phase 8.1 현재 라이브 섹션 구현 완료
|
||||||
|
|
||||||
|
- 목적: V2 Main Home 추천 탭에 현재 라이브 섹션을 API 응답 데이터 기반으로 표시
|
||||||
|
- 수행 내용:
|
||||||
|
- `MainHomeLiveSection`을 `Recommendation/Components` 아래에 추가하고 `HomeLiveItem.roomId`, `creatorNickname`, `creatorProfileImage`를 사용해 렌더링
|
||||||
|
- Figma `24:5518` 라이브 아이템 메타데이터(`70x102`)와 스크린샷을 기준으로 단일 아이템 프레임과 프로필 이미지/라이브 배지 배치를 보정
|
||||||
|
- `MainHomeLiveSection_Previews`에 `Live Item` 고정 크기 Preview와 `Live Section` Preview를 추가해 Xcode Preview에서 확인 가능하도록 구성
|
||||||
|
- 섹션 제목을 제거하고 `roomId`를 non-optional 응답 값으로 반영
|
||||||
|
- 데이터가 비어 있으면 `SectionTitle`과 가로 목록이 렌더링되지 않도록 구성
|
||||||
|
- `MainHomeRecommendationView`에는 Phase 8 현재 라이브 섹션만 조립하고, 아이템 탭 시 `MainView`에서 로그인 guard 후 `LiveViewModel.enterLiveRoom(roomId:)`로 전달되도록 연결
|
||||||
|
- `MainView`에 이미 배치된 `LivePaymentDialog`, `LiveRoomPasswordDialog`, `LiveRoomViewV2` 표시 흐름을 재사용
|
||||||
|
- 신규 Swift 파일을 `SodaLive.xcodeproj/project.pbxproj` 빌드 대상에 포함
|
||||||
|
- 검증:
|
||||||
|
- Figma `24:5518` 메타데이터 확인 결과: `live` 노드 크기 `70x102`, 스크린샷 기준 원형 프로필/하단 `LIVE` 배지/닉네임 말줄임 구성 확인
|
||||||
|
- `rg "MainHomeLiveSection|roomId|creatorNickname|creatorProfileImage|enterLiveRoom|LivePaymentDialog|LiveRoomPasswordDialog" SodaLive/Sources/V2/Main/Home/Recommendation SodaLive/Sources/V2/Main/MainView.swift` 실행, 라이브 섹션 필드와 기존 입장 흐름 재사용 확인
|
||||||
|
- `plutil -lint SodaLive.xcodeproj/project.pbxproj` 실행, `OK` 확인
|
||||||
|
- `git diff --check` 실행, 출력 없이 성공 확인
|
||||||
|
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` 실행, `BUILD SUCCEEDED` 확인
|
||||||
|
|
||||||
|
### 2026-06-29 추천 응답 모델 optionality 정렬
|
||||||
|
|
||||||
|
- 목적: PRD의 "서버 nullable 필드는 Swift optional" 기준에 맞춰 `MainHomeRecommendationResponse.swift` 모델 optionality를 정리
|
||||||
|
- 수행 내용:
|
||||||
|
- PRD 응답 스펙에서 `?`가 없는 필드를 Swift non-optional로 변경
|
||||||
|
- `targetId`, `coverImage`, `profileImage`, `originalWorkTitle`, `creatorProfileImage`, `imageUrl`, `audioUrl` 등 PRD nullable 필드는 optional 유지
|
||||||
|
- `RecommendationBannerResponse`는 상세 PRD 스펙이 별도로 확정되지 않아 기존 optional 선언 유지
|
||||||
|
- `HomeLiveItem.creatorNickname`, `creatorProfileImage`가 non-optional이 되면서 라이브 섹션의 불필요한 nil fallback 제거
|
||||||
|
- 검증:
|
||||||
|
- `rg -n "let (roomId|activityType|activityAt|contentId|isPointAvailable|characterId|totalChatCount|genreName|creators|postId|createdAt|likeCount|commentCount|existOrdered): .*\\?" SodaLive/Sources/V2/Main/Home/Recommendation/Models/MainHomeRecommendationResponse.swift` 실행, 검색 결과 없음 확인 예정
|
||||||
|
- `rg -n "\\?\\? \"\"" SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeLiveSection.swift` 실행, 검색 결과 없음 확인 예정
|
||||||
|
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` 실행 예정
|
||||||
|
|
||||||
### 2026-06-29 Phase 7 중간 빌드 검증 완료
|
### 2026-06-29 Phase 7 중간 빌드 검증 완료
|
||||||
|
|
||||||
- 목적: Phase 4~6에서 생성/이동한 데이터 상태, 라우팅, 탭 루트 Swift 파일이 프로젝트 빌드 대상에 포함되어 있는지 중간 검증
|
- 목적: Phase 4~6에서 생성/이동한 데이터 상태, 라우팅, 탭 루트 Swift 파일이 프로젝트 빌드 대상에 포함되어 있는지 중간 검증
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ enum class RecommendedActivityType(val code: String) {
|
|||||||
- 본인인증 성공 후 `pendingAction`을 실행해 원래 상세 화면으로 이동한다.
|
- 본인인증 성공 후 `pendingAction`을 실행해 원래 상세 화면으로 이동한다.
|
||||||
- 민감 콘텐츠 보기 설정이 꺼져 있으면 `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` 입장을 재사용한다.
|
||||||
|
|
||||||
라이브 항목 주의사항:
|
라이브 항목 주의사항:
|
||||||
- 기존 홈의 라이브 guard는 `roomId`와 `isAdult`를 함께 받아 성인 여부를 판단한다.
|
- 기존 홈의 라이브 guard는 `roomId`와 `isAdult`를 함께 받아 성인 여부를 판단한다.
|
||||||
|
|||||||
@@ -33,6 +33,9 @@
|
|||||||
- 프로퍼티 선언, 비즈니스 로직, 헬퍼 메서드는 공백 줄로 구획한다.
|
- 프로퍼티 선언, 비즈니스 로직, 헬퍼 메서드는 공백 줄로 구획한다.
|
||||||
- 클로저 체인은 줄바꿈해 가독성을 유지한다.
|
- 클로저 체인은 줄바꿈해 가독성을 유지한다.
|
||||||
|
|
||||||
|
## SwiftUI Preview 규칙
|
||||||
|
- Preview에서 이미지 URL 샘플이 필요하면 `https://placehold.co/500`를 사용한다.
|
||||||
|
|
||||||
## 타입/상태 관리
|
## 타입/상태 관리
|
||||||
- ViewModel은 `final class ...: ObservableObject` 패턴을 우선한다.
|
- ViewModel은 `final class ...: ObservableObject` 패턴을 우선한다.
|
||||||
- View가 소유하는 객체는 `@StateObject`, 외부 주입 객체는 `@ObservedObject`를 사용한다.
|
- View가 소유하는 객체는 `@StateObject`, 외부 주입 객체는 `@ObservedObject`를 사용한다.
|
||||||
|
|||||||
Reference in New Issue
Block a user