Files
sodalive-ios/docs/plan-task/20260519_메인페이지신규개발.md
2026-05-19 15:54:37 +09:00

22 KiB

Main Page Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: , 콘텐츠, 채팅, 마이 4개 하단 탭을 가진 신규 메인 페이지를 SodaLive/Sources/V2/Main/** 아래에 구현하고 앱 루트에 연결한다.

Architecture: 신규 MainView를 기존 HomeView와 독립된 메인 컨테이너로 만든다. 탭 상태, 탭바, 빈 탭 화면을 V2/Main 모듈로 분리하고, 기존 HomeView의 미니 플레이어/인증/팝업 흐름은 새 컨테이너에 필요한 범위만 이관한다. 마이 탭은 기존 MyPageView를 그대로 재사용한다.

Tech Stack: SwiftUI, Combine, AppState 기반 전역 NavigationStack, Xcode project source membership, 기존 I18n 문자열 시스템


기준 문서

  • PRD: docs/prd/20260519_메인페이지신규개발_PRD.md
  • 검증 가이드: docs/agent-guides/build-test-verification.md
  • 코드 스타일: docs/agent-guides/code-style.md

파일 구조

생성

  • SodaLive/Sources/V2/Main/MainTab.swift: 4개 탭 정의, 타이틀, 선택/미선택 아이콘 매핑
  • SodaLive/Sources/V2/Main/MainViewModel.swift: 현재 선택 탭 상태 관리
  • SodaLive/Sources/V2/Main/MainView.swift: 신규 메인 루트 컨테이너, 탭 콘텐츠 전환, 미니 플레이어/팝업 이관 지점
  • SodaLive/Sources/V2/Main/MainTabBarView.swift: 하단 탭바 HStack 구성
  • SodaLive/Sources/V2/Main/MainTabBarButton.swift: 탭 버튼 단일 UI, 정렬 규칙 적용
  • SodaLive/Sources/V2/Main/MainPlaceholderTabView.swift: 검정 배경 + 탭명 표시 빈 페이지

수정

  • SodaLive/Sources/ContentView.swift: 루트 화면을 HomeView()에서 MainView()로 변경
  • SodaLive/Sources/I18n/I18n.swift: I18n.Main.Tab.content 추가
  • SodaLive.xcodeproj/project.pbxproj: 신규 Swift 파일 6개를 SodaLive, SodaLive-dev 앱 타깃 Sources에 포함

수정하지 않음

  • SodaLive/Sources/MyPage/MyPageView.swift: 기존 마이페이지는 재사용만 한다.
  • SodaLive/Sources/Main/Home/HomeView.swift: 신규 MainView 연결이 안정화되기 전에는 삭제하거나 대규모 정리하지 않는다.
  • SodaLive/Sources/App/AppState.swift, SodaLive/Sources/App/AppStep.swift: .main 의미와 전역 라우팅 구조는 유지한다.

구현 체크리스트

Task 1: MainTab 모델 작성

Files:

  • Create: SodaLive/Sources/V2/Main/MainTab.swift

  • Step 1: MainTab.swift 생성

//
//  MainTab.swift
//  SodaLive
//

import Foundation

enum MainTab: CaseIterable, Hashable {
    case home
    case content
    case chat
    case my

    var title: String {
        switch self {
        case .home:
            return I18n.Main.Tab.home
        case .content:
            return I18n.Main.Tab.content
        case .chat:
            return I18n.Main.Tab.chat
        case .my:
            return I18n.Main.Tab.my
        }
    }

    var selectedIconName: String {
        switch self {
        case .home:
            return "ic_nav_home_selected"
        case .content:
            return "ic_nav_content_selected"
        case .chat:
            return "ic_nav_chat_selected"
        case .my:
            return "ic_tabbar_my_selected"
        }
    }

    var unselectedIconName: String {
        switch self {
        case .home:
            return "ic_nav_home"
        case .content:
            return "ic_nav_content"
        case .chat:
            return "ic_nav_chat"
        case .my:
            return "ic_nav_my"
        }
    }
}
  • Step 2: 정적 진단 실행

Run: lsp_diagnostics("SodaLive/Sources/V2/Main/MainTab.swift")

Expected: 신규 파일 자체의 Swift 문법 오류가 없어야 한다. SourceKit이 프로젝트 컨텍스트를 완전히 잡지 못해 외부 심볼 오류를 보고하면 이후 빌드로 최종 검증한다.

Task 2: 다국어 탭 라벨 추가

Files:

  • Modify: SodaLive/Sources/I18n/I18n.swift

  • Step 1: I18n.Main.Tab.content 추가

I18n.Main.Tab에 아래 항목을 추가한다.

static var content: String { pick(ko: "콘텐츠", en: "Content", ja: "コンテンツ") }

적용 후 I18n.Main.Tab은 아래 형태가 된다.

enum Tab {
    static var home: String { pick(ko: "홈", en: "Home", ja: "ホーム") }
    static var content: String { pick(ko: "콘텐츠", en: "Content", ja: "コンテンツ") }
    static var live: String { pick(ko: "라이브", en: "Live", ja: "ライブ") }
    static var chat: String { pick(ko: "채팅", en: "Chat", ja: "チャット") }
    static var my: String { pick(ko: "마이", en: "My", ja: "マイ") }
}
  • Step 2: 정적 진단 실행

Run: lsp_diagnostics("SodaLive/Sources/I18n/I18n.swift")

Expected: 변경 구간의 중괄호/스코프 오류가 없어야 한다.

Task 3: 빈 탭 페이지 작성

Files:

  • Create: SodaLive/Sources/V2/Main/MainPlaceholderTabView.swift

  • Step 1: 검정 배경 + 탭명 표시 View 생성

//
//  MainPlaceholderTabView.swift
//  SodaLive
//

import SwiftUI

struct MainPlaceholderTabView: View {
    let title: String

    var body: some View {
        ZStack {
            Color.black
                .ignoresSafeArea()

            Text(title)
                .appFont(size: 20, weight: .bold)
                .foregroundColor(.white)
        }
    }
}

struct MainPlaceholderTabView_Previews: PreviewProvider {
    static var previews: some View {
        MainPlaceholderTabView(title: "홈")
    }
}
  • Step 2: 정적 진단 실행

Run: lsp_diagnostics("SodaLive/Sources/V2/Main/MainPlaceholderTabView.swift")

Expected: 문법 오류가 없어야 한다.

Task 4: 탭 버튼 UI 작성

Files:

  • Create: SodaLive/Sources/V2/Main/MainTabBarButton.swift

  • Step 1: 정렬 기준을 반영한 버튼 View 생성

//
//  MainTabBarButton.swift
//  SodaLive
//

import SwiftUI

struct MainTabBarButton: View {
    let tab: MainTab
    let isSelected: Bool
    let width: CGFloat
    let action: () -> Void

    var body: some View {
        Button(action: action) {
            VStack(spacing: 4) {
                ZStack(alignment: .center) {
                    Image(isSelected ? tab.selectedIconName : tab.unselectedIconName)
                }
                .frame(height: 24)

                Text(tab.title)
                    .appFont(size: 10, weight: isSelected ? .bold : .medium)
                    .foregroundColor(isSelected ? Color.button : Color.graybb)
                    .frame(height: 12, alignment: .bottom)
            }
            .frame(width: width, minHeight: 50, alignment: .center)
            .contentShape(Rectangle())
        }
        .buttonStyle(.plain)
    }
}

struct MainTabBarButton_Previews: PreviewProvider {
    static var previews: some View {
        MainTabBarButton(
            tab: .home,
            isSelected: true,
            width: UIScreen.main.bounds.width / 4,
            action: {}
        )
        .background(Color.gray11)
    }
}
  • Step 2: 정적 진단 실행

Run: lsp_diagnostics("SodaLive/Sources/V2/Main/MainTabBarButton.swift")

Expected: 문법 오류가 없어야 한다.

Task 5: 하단 탭바 작성

Files:

  • Create: SodaLive/Sources/V2/Main/MainTabBarView.swift

  • Step 1: 4분할 탭바 View 생성

//
//  MainTabBarView.swift
//  SodaLive
//

import SwiftUI

struct MainTabBarView: View {
    let width: CGFloat
    @Binding var currentTab: MainTab

    var body: some View {
        HStack(spacing: 0) {
            let tabWidth = width / CGFloat(MainTab.allCases.count)

            ForEach(MainTab.allCases, id: \.self) { tab in
                MainTabBarButton(
                    tab: tab,
                    isSelected: currentTab == tab,
                    width: tabWidth,
                    action: {
                        currentTab = tab
                    }
                )
            }
        }
        .padding(.top, 8)
        .padding(.bottom, 8)
        .background(Color.gray11)
    }
}

struct MainTabBarView_Previews: PreviewProvider {
    static var previews: some View {
        MainTabBarView(
            width: UIScreen.main.bounds.width,
            currentTab: .constant(.home)
        )
    }
}
  • Step 2: 정적 진단 실행

Run: lsp_diagnostics("SodaLive/Sources/V2/Main/MainTabBarView.swift")

Expected: 문법 오류가 없어야 한다.

Task 6: 메인 ViewModel 작성

Files:

  • Create: SodaLive/Sources/V2/Main/MainViewModel.swift

  • Step 1: 탭 상태 ViewModel 생성

//
//  MainViewModel.swift
//  SodaLive
//

import Combine
import Foundation

final class MainViewModel: ObservableObject {
    @Published var currentTab: MainTab = .home
}
  • Step 2: 정적 진단 실행

Run: lsp_diagnostics("SodaLive/Sources/V2/Main/MainViewModel.swift")

Expected: 문법 오류가 없어야 한다.

Task 7: 신규 MainView 1차 작성

Files:

  • Create: SodaLive/Sources/V2/Main/MainView.swift

  • Step 1: 탭 전환 컨테이너 생성

//
//  MainView.swift
//  SodaLive
//

import SwiftUI

struct MainView: View {
    @StateObject private var viewModel = MainViewModel()

    var body: some View {
        GeometryReader { proxy in
            VStack(spacing: 0) {
                contentView

                MainTabBarView(
                    width: proxy.size.width,
                    currentTab: $viewModel.currentTab
                )

                if proxy.safeAreaInsets.bottom > 0 {
                    Rectangle()
                        .foregroundColor(Color.gray11)
                        .frame(width: proxy.size.width, height: 15.3)
                }
            }
            .background(Color.black)
        }
    }

    @ViewBuilder
    private var contentView: some View {
        switch viewModel.currentTab {
        case .home:
            MainPlaceholderTabView(title: MainTab.home.title)
        case .content:
            MainPlaceholderTabView(title: MainTab.content.title)
        case .chat:
            MainPlaceholderTabView(title: MainTab.chat.title)
        case .my:
            MyPageView()
        }
    }
}

struct MainView_Previews: PreviewProvider {
    static var previews: some View {
        MainView()
    }
}
  • Step 2: 정적 진단 실행

Run: lsp_diagnostics("SodaLive/Sources/V2/Main/MainView.swift")

Expected: 문법 오류가 없어야 한다.

Task 8: 기존 메인 주변 기능 이관

Files:

  • Modify: SodaLive/Sources/V2/Main/MainView.swift

  • Reference: SodaLive/Sources/Main/Home/HomeView.swift

  • Step 1: 미니 플레이어 상태와 매니저 이관

MainView에 기존 HomeView의 플레이어 관련 상태를 추가한다.

@StateObject private var appState = AppState.shared
@StateObject private var contentPlayManager = ContentPlayManager.shared
@StateObject private var contentPlayerPlayManager = ContentPlayerPlayManager.shared

@State private var isShowPlayer = false

VStack에서 콘텐츠와 탭바 사이에 기존 HomeView의 두 미니 플레이어 블록을 동일한 조건으로 배치한다.

if contentPlayerPlayManager.isShowingMiniPlayer {
    contentPlayerMiniPlayerView
}

if contentPlayManager.isShowingMiniPlayer {
    previewContentMiniPlayerView
}

contentPlayerMiniPlayerViewHomeView.swiftcontentPlayerPlayManager 기반 블록과 동일한 동작을 유지한다. previewContentMiniPlayerViewcontentPlayManager 기반 블록과 동일하게 contentDetail 이동, 재생/일시정지, 정지를 처리한다.

  • Step 2: 본인인증 관련 상태와 Bootpay payload 이관

기존 HomeView의 인증 관련 상태를 MainView에 추가한다.

@AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
@AppStorage("auth") private var auth: Bool = UserDefaults.bool(forKey: UserDefaultsKey.auth)

@State private var isShowAuthView = false
@State private var isShowAuthConfirmView = false
@State private var pendingAction: (() -> Void)? = nil
@State private var payload = Payload()

Bootpay, BootpayUI import가 필요한 경우 MainView.swift 상단에 추가한다.

import Bootpay
import BootpayUI

onAppear에서 기존 payload 초기화 값을 유지한다.

payload.applicationId = BOOTPAY_APP_ID
payload.price = 0
payload.pg = "다날"
payload.method = "본인인증"
payload.orderName = "본인인증"
payload.authenticationId = "\(UserDefaults.string(forKey: .nickname))__\(String(NSTimeIntervalSince1970))"
  • Step 3: 전역 팝업/다이얼로그 이관

기존 HomeView에서 메인 루트 차원에 표시되던 아래 UI를 MainView의 최상위 ZStack으로 옮긴다.

  • NotificationSettingsDialog()
  • 본인인증 안내 SodaDialog
  • LivePaymentDialog
  • LiveRoomPasswordDialog
  • appState.eventPopup 기반 이벤트 팝업

LivePaymentDialog, LiveRoomPasswordDialog 유지에 필요한 LiveViewModel은 기존처럼 MainView가 소유한다.

@StateObject private var liveViewModel = LiveViewModel()
  • Step 4: 사용자 정보 갱신/이벤트 팝업 조회 이관 범위 결정대로 반영

기존 HomeViewHomeViewModel 의존을 신규 MainViewModel로 무리하게 옮기지 않는다. 기존 메인 진입 시 필요한 부수효과가 있으면 HomeViewModel을 임시로 MainView에서 소유해 기존 메서드를 호출한다.

@StateObject private var legacyHomeViewModel = HomeViewModel()

onAppear에서 기존 호출을 유지한다.

if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
    pushTokenUpdate()
    legacyHomeViewModel.getMemberInfo()
    legacyHomeViewModel.getEventPopup()
    legacyHomeViewModel.addAllPlaybackTracking()
}

pushTokenUpdate()는 기존 HomeView의 메서드를 동일하게 옮겨 Messaging.messaging().token을 조회하고 legacyHomeViewModel.pushTokenUpdate(pushToken:)를 호출한다.

  • Step 5: 정적 진단 실행

Run: lsp_diagnostics("SodaLive/Sources/V2/Main/MainView.swift")

Expected: 문법 오류가 없어야 한다. 외부 SDK 심볼 인식 한계가 있으면 빌드로 최종 검증한다.

Task 9: 앱 루트 연결

Files:

  • Modify: SodaLive/Sources/ContentView.swift

  • Step 1: 루트 화면 교체

ContentView의 루트 렌더링을 아래처럼 변경한다.

if appState.isRestartApp {
    EmptyView()
} else {
    MainView()
}
  • Step 2: 정적 진단 실행

Run: lsp_diagnostics("SodaLive/Sources/ContentView.swift")

Expected: MainView 참조 오류가 없어야 한다. Xcode project membership이 아직 반영되지 않아 인식 실패하면 Task 10 이후 다시 확인한다.

Task 10: Xcode project membership 반영

Files:

  • Modify: SodaLive.xcodeproj/project.pbxproj

  • Step 1: 신규 Swift 파일을 프로젝트에 포함

아래 6개 파일을 Xcode project의 SodaLiveSodaLive-dev Sources build phase에 포함한다.

  • MainTab.swift
  • MainViewModel.swift
  • MainView.swift
  • MainTabBarView.swift
  • MainTabBarButton.swift
  • MainPlaceholderTabView.swift

기존 project.pbxproj는 동일 파일이 두 앱 타깃에 각각 PBXBuildFile로 등록되는 패턴을 사용한다. 새 파일도 같은 패턴으로 추가한다.

  • Step 2: 프로젝트 파일 참조 확인

Run: rg -n "MainTab.swift|MainViewModel.swift|MainView.swift|MainTabBarView.swift|MainTabBarButton.swift|MainPlaceholderTabView.swift" "SodaLive.xcodeproj/project.pbxproj"

Expected: 각 파일명이 PBXFileReference, PBXBuildFile, PBXSourcesBuildPhase 구간에 나타난다.

Task 11: 기능 검증

Files:

  • Verify: SodaLive/Sources/V2/Main/*.swift

  • Verify: SodaLive/Sources/ContentView.swift

  • Verify: SodaLive/Sources/I18n/I18n.swift

  • Verify: SodaLive.xcodeproj/project.pbxproj

  • Step 1: 변경 파일 정적 진단 실행

Run each:

lsp_diagnostics("SodaLive/Sources/V2/Main/MainTab.swift")
lsp_diagnostics("SodaLive/Sources/V2/Main/MainViewModel.swift")
lsp_diagnostics("SodaLive/Sources/V2/Main/MainView.swift")
lsp_diagnostics("SodaLive/Sources/V2/Main/MainTabBarView.swift")
lsp_diagnostics("SodaLive/Sources/V2/Main/MainTabBarButton.swift")
lsp_diagnostics("SodaLive/Sources/V2/Main/MainPlaceholderTabView.swift")
lsp_diagnostics("SodaLive/Sources/ContentView.swift")
lsp_diagnostics("SodaLive/Sources/I18n/I18n.swift")

Expected: 변경으로 인한 Swift 문법/타입 오류가 없어야 한다. SourceKit이 외부 모듈을 못 찾는 기존 한계는 빌드 결과로 판정한다.

  • Step 2: 앱 빌드 실행

Run: xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build

Expected: ** BUILD SUCCEEDED **

  • Step 3: dev 앱 빌드 실행

Run: xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build

Expected: ** BUILD SUCCEEDED **

  • Step 4: 테스트 액션 상태 확인

Run: xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test

Expected: 현재 저장소 상태상 테스트 번들 타깃이 없으면 Scheme SodaLive is not currently configured for the test action.가 나올 수 있다. 이 경우 빌드 성공을 컴파일 검증 기준으로 삼고, 테스트 액션 부재를 검증 기록에 남긴다.

Task 12: 수동 확인 항목

Files:

  • Verify: 실행 앱 화면

  • Step 1: 메인 탭 표시 확인

확인 기준:

  • 하단 탭 순서가 , 콘텐츠, 채팅, 마이다.

  • 각 탭은 화면 폭의 1/4을 사용한다.

  • 아이콘과 타이틀은 가로 가운데 정렬이다.

  • 타이틀 하단 라인이 탭 간 흔들리지 않는다.

  • Step 2: 탭별 화면 확인

확인 기준:

  • 탭: 검정 배경, 텍스트 표시

  • 콘텐츠 탭: 검정 배경, 콘텐츠 텍스트 표시

  • 채팅 탭: 검정 배경, 채팅 텍스트 표시

  • 마이 탭: 기존 MyPageView 표시

  • Step 3: 아이콘 매핑 확인

확인 기준:

  • : ic_nav_home_selected / ic_nav_home
  • 콘텐츠: ic_nav_content_selected / ic_nav_content
  • 채팅: ic_nav_chat_selected / ic_nav_chat
  • 마이: ic_tabbar_my_selected / ic_nav_my

검증 기록

  • 계획 문서 작성 단계에서는 코드 구현을 수행하지 않았다.
  • 계획 작성 전 확인한 근거 파일: ContentView.swift, HomeView.swift, HomeViewModel.swift, BottomTabView.swift, TabButton.swift, MyPageView.swift, I18n.swift, SodaLive.xcodeproj/project.pbxproj.
  • 구현 완료 후 검증 기록은 이 섹션 아래에 실행 명령과 결과를 누적한다.
  • 무엇/왜/어떻게: 신규 V2/Main Swift 파일 6개, I18n.Main.Tab.content, ContentView 루트 연결, Xcode project membership을 계획 문서 기준으로 구현했다.
  • 실행 명령: lsp_diagnostics (SodaLive/Sources/V2/Main, ContentView.swift, I18n.swift)
  • 결과: SourceKit 단독 컨텍스트 한계로 기존 프로젝트 심볼/외부 모듈 인식 오류가 보고되었고, 실제 컴파일 유효성은 빌드로 검증했다.
  • 실행 명령: rg -n "MainTab.swift|MainViewModel.swift|MainView.swift|MainTabBarView.swift|MainTabBarButton.swift|MainPlaceholderTabView.swift|41A00013" "SodaLive.xcodeproj/project.pbxproj"
  • 결과: 신규 파일 6개가 PBXFileReference, PBXBuildFile, V2/Main 그룹, 두 앱 타깃 PBXSourcesBuildPhase에 포함되어 있음을 확인했다.
  • 실행 명령: xcodebuild -workspace "SodaLive.xcworkspace" -list
  • 결과: workspace/project 파싱 성공, SodaLive, SodaLive-dev 스킴 확인.
  • 실행 명령: xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build
  • 결과: ** BUILD SUCCEEDED **.
  • 실행 명령: xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build
  • 결과: ** BUILD SUCCEEDED **.
  • 실행 명령: xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test
  • 결과: Scheme SodaLive is not currently configured for the test action.
  • 실행 명령: xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -destination "platform=iOS Simulator,name=iPhone 17,OS=26.0" build
  • 결과: ** BUILD SUCCEEDED **.
  • 실행 명령: xcrun simctl boot "iPhone 17" && xcrun simctl install "iPhone 17" ".../Debug-iphonesimulator/SodaLive-dev.app" && xcrun simctl launch "iPhone 17" "kr.co.vividnext.sodalive.debug2"
  • 결과: 앱 설치 및 실행 성공, launch pid 75253 확인.
  • 실행 명령: xcrun simctl io "iPhone 17" screenshot "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/sodalive-main.png"
  • 결과: 시뮬레이터 실행 화면 스크린샷 저장 완료.
  • 무엇/왜/어떻게: 구현 후 코드 리뷰를 요청해 요구사항 누락과 품질 이슈를 점검했다.
  • 실행 명령/도구: task(category="deep") 코드 리뷰 및 follow-up 검토
  • 결과: 현재 범위는 홈/콘텐츠/채팅/마이홈/콘텐츠/채팅이 placeholder이므로 Bootpay 인증 트리거가 없는 것은 blocking issue가 아니며, 향후 실제 라이브/성인 콘텐츠 진입 UI 연결 시 isShowAuthConfirmView/pendingAction 경로를 연결해야 한다는 non-blocking future integration note로 정리했다.

커밋 정책

  • 이 계획 실행 중 커밋은 사용자가 명시적으로 요청한 경우에만 수행한다.
  • 커밋 요청이 있으면 먼저 commit-policy 스킬을 로드하고, 커밋 직후 work/scripts/check-commit-message-rules.sh를 실행한다.