feat(home): 현재 라이브 섹션을 연결한다

This commit is contained in:
Yu Sung
2026-06-29 13:22:00 +09:00
parent e67efeb30f
commit c441fb0b02
8 changed files with 191 additions and 38 deletions

View File

@@ -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 */,

View File

@@ -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")
}
}
}

View File

@@ -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 {

View File

@@ -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
} }

View File

@@ -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: {}
)
} }
} }

View File

@@ -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 파일이 프로젝트 빌드 대상에 포함되어 있는지 중간 검증

View File

@@ -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`를 함께 받아 성인 여부를 판단한다.

View File

@@ -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`를 사용한다.