diff --git a/SodaLive/Resources/Assets.xcassets/v2/ic_bar_bell.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/v2/ic_bar_bell.imageset/Contents.json new file mode 100644 index 0000000..3ad6f50 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/v2/ic_bar_bell.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_bar_bell.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/v2/ic_bar_bell.imageset/ic_bar_bell.png b/SodaLive/Resources/Assets.xcassets/v2/ic_bar_bell.imageset/ic_bar_bell.png new file mode 100644 index 0000000..2490d6b Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/v2/ic_bar_bell.imageset/ic_bar_bell.png differ diff --git a/SodaLive/Resources/Assets.xcassets/v2/ic_bar_cash.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/v2/ic_bar_cash.imageset/Contents.json new file mode 100644 index 0000000..c21a0af --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/v2/ic_bar_cash.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_bar_cash.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/v2/ic_bar_cash.imageset/ic_bar_cash.png b/SodaLive/Resources/Assets.xcassets/v2/ic_bar_cash.imageset/ic_bar_cash.png new file mode 100644 index 0000000..43dd80b Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/v2/ic_bar_cash.imageset/ic_bar_cash.png differ diff --git a/SodaLive/Resources/Assets.xcassets/v2/ic_bar_search.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/v2/ic_bar_search.imageset/Contents.json new file mode 100644 index 0000000..c6cc406 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/v2/ic_bar_search.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_bar_search.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/v2/ic_bar_search.imageset/ic_bar_search.png b/SodaLive/Resources/Assets.xcassets/v2/ic_bar_search.imageset/ic_bar_search.png new file mode 100644 index 0000000..d3da8e4 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/v2/ic_bar_search.imageset/ic_bar_search.png differ diff --git a/SodaLive/Resources/Assets.xcassets/v2/img_text_logo_v2.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/v2/img_text_logo_v2.imageset/Contents.json new file mode 100644 index 0000000..3b387aa --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/v2/img_text_logo_v2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "img_text_logo_v2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/v2/img_text_logo_v2.imageset/img_text_logo_v2.png b/SodaLive/Resources/Assets.xcassets/v2/img_text_logo_v2.imageset/img_text_logo_v2.png new file mode 100644 index 0000000..c6d7135 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/v2/img_text_logo_v2.imageset/img_text_logo_v2.png differ diff --git a/SodaLive/Sources/V2/Component/CapsuleTabBar.swift b/SodaLive/Sources/V2/Component/CapsuleTabBar.swift new file mode 100644 index 0000000..130431b --- /dev/null +++ b/SodaLive/Sources/V2/Component/CapsuleTabBar.swift @@ -0,0 +1,78 @@ +// +// CapsuleTabBar.swift +// SodaLive +// + +import SwiftUI + +struct CapsuleTabBar: View { + let items: [Item] + @Binding var selectedItem: Item + let title: (Item) -> String + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack(alignment: .center, spacing: SodaSpacing.s8) { + ForEach(items, id: \.self) { item in + CapsuleTabBarItem( + title: title(item), + isSelected: selectedItem == item, + action: { + selectedItem = item + } + ) + } + } + .padding(.horizontal, SodaSpacing.s20) + } + .frame(maxWidth: .infinity) + .frame(height: 52, alignment: .center) + .background(Color.black) + } +} + +private struct CapsuleTabBarItem: View { + let title: String + let isSelected: Bool + let action: () -> Void + + var body: some View { + Button { + action() + } label: { + Text(title) + .appFont(.body5) + .foregroundColor(Color.white) + .lineLimit(1) + .padding(.horizontal, SodaSpacing.s12) + .padding(.vertical, SodaSpacing.s8) + .frame(height: 34) + .background(isSelected ? Color.soda400 : Color.black) + .clipShape(Capsule()) + .overlay( + Capsule() + .stroke(isSelected ? Color.soda400 : Color.gray700, lineWidth: 1) + ) + } + .buttonStyle(.plain) + } +} + +struct CapsuleTabBar_Previews: PreviewProvider { + private enum PreviewTab: String, CaseIterable { + case recommended = "추천" + case ranking = "랭킹" + case following = "팔로잉" + case live = "라이브" + case content = "콘텐츠" + case audition = "오디션" + } + + static var previews: some View { + CapsuleTabBar( + items: PreviewTab.allCases, + selectedItem: .constant(.recommended), + title: { $0.rawValue } + ) + } +} diff --git a/SodaLive/Sources/V2/Component/DefaultTitleBar.swift b/SodaLive/Sources/V2/Component/DefaultTitleBar.swift new file mode 100644 index 0000000..2373d1a --- /dev/null +++ b/SodaLive/Sources/V2/Component/DefaultTitleBar.swift @@ -0,0 +1,39 @@ +// +// DefaultTitleBar.swift +// SodaLive +// + +import SwiftUI + +struct DefaultTitleBar: View { + let title: String + private let menu: Menu + + init( + title: String, + @ViewBuilder menu: () -> Menu + ) { + self.title = title + self.menu = menu() + } + + var body: some View { + TitleBar { + Text(title) + .appFont(.heading2) + .foregroundColor(.white) + .lineLimit(1) + .truncationMode(.tail) + } trailing: { + menu + } + } +} + +struct DefaultTitleBar_Previews: PreviewProvider { + static var previews: some View { + DefaultTitleBar(title: "화면명") { + Image("ic_bar_search") + } + } +} diff --git a/SodaLive/Sources/V2/Component/HomeTitleBar.swift b/SodaLive/Sources/V2/Component/HomeTitleBar.swift new file mode 100644 index 0000000..7d0cb41 --- /dev/null +++ b/SodaLive/Sources/V2/Component/HomeTitleBar.swift @@ -0,0 +1,32 @@ +// +// HomeTitleBar.swift +// SodaLive +// + +import SwiftUI + +struct HomeTitleBar: View { + private let menu: Menu + + init(@ViewBuilder menu: () -> Menu) { + self.menu = menu() + } + + var body: some View { + TitleBar { + Image("img_text_logo_v2") + .resizable() + .scaledToFit() + } trailing: { + menu + } + } +} + +struct HomeTitleBar_Previews: PreviewProvider { + static var previews: some View { + HomeTitleBar { + Image("ic_bar_bell") + } + } +} diff --git a/SodaLive/Sources/V2/Component/TextTabBar.swift b/SodaLive/Sources/V2/Component/TextTabBar.swift new file mode 100644 index 0000000..71ed5e3 --- /dev/null +++ b/SodaLive/Sources/V2/Component/TextTabBar.swift @@ -0,0 +1,51 @@ +// +// TextTabBar.swift +// SodaLive +// + +import SwiftUI + +struct TextTabBar: View { + let items: [Item] + @Binding var selectedItem: Item + let title: (Item) -> String + + var body: some View { + HStack(alignment: .center, spacing: SodaSpacing.s20) { + ForEach(items.prefix(3), id: \.self) { item in + Button { + selectedItem = item + } label: { + Text(title(item)) + .appFont(.heading3) + .foregroundColor(selectedItem == item ? Color.white : Color.gray600) + .frame(height: 52, alignment: .center) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + + Spacer() + } + .padding(.horizontal, SodaSpacing.s20) + .frame(maxWidth: .infinity) + .frame(height: 52, alignment: .leading) + .background(Color.black) + } +} + +struct TextTabBar_Previews: PreviewProvider { + private enum PreviewTab: String, CaseIterable { + case recommended = "추천" + case ranking = "랭킹" + case following = "팔로잉" + } + + static var previews: some View { + TextTabBar( + items: PreviewTab.allCases, + selectedItem: .constant(.recommended), + title: { $0.rawValue } + ) + } +} diff --git a/SodaLive/Sources/V2/Component/TitleBar.swift b/SodaLive/Sources/V2/Component/TitleBar.swift new file mode 100644 index 0000000..269522d --- /dev/null +++ b/SodaLive/Sources/V2/Component/TitleBar.swift @@ -0,0 +1,45 @@ +// +// TitleBar.swift +// SodaLive +// + +import SwiftUI + +struct TitleBar: View { + private let leading: Leading + private let trailing: Trailing + + init( + @ViewBuilder leading: () -> Leading, + @ViewBuilder trailing: () -> Trailing + ) { + self.leading = leading() + self.trailing = trailing() + } + + var body: some View { + HStack(alignment: .center, spacing: 0) { + leading + + Spacer(minLength: 0) + + trailing + } + .padding(.horizontal, SodaSpacing.s20) + .frame(maxWidth: .infinity) + .frame(height: 60, alignment: .center) + .background(Color.black) + } +} + +struct TitleBar_Previews: PreviewProvider { + static var previews: some View { + TitleBar { + Text("Title") + .appFont(.heading3) + .foregroundColor(.white) + } trailing: { + Image("ic_bar_search") + } + } +} diff --git a/docs/plan-task/20260519_재사용타이틀탭텍스트바컴포넌트.md b/docs/plan-task/20260519_재사용타이틀탭텍스트바컴포넌트.md new file mode 100644 index 0000000..0ea00e1 --- /dev/null +++ b/docs/plan-task/20260519_재사용타이틀탭텍스트바컴포넌트.md @@ -0,0 +1,133 @@ +# Reusable Title Bar and Tab-Text Bar Component Plan + +> **For agentic workers:** 구현 시 문서 범위를 벗어나지 않는다. 신규 UI 컴포넌트 작업이므로 구현 단계에서는 `frontend-design` 또는 시각/UI 전용 실행 경로를 사용한다. 이 문서는 현재 요청에 따라 문서만 생성하며 코드 변경은 포함하지 않는다. + +**Goal:** `Home Title Bar`, `Default Title Bar`, `Tab-Text Bar`를 SwiftUI 재사용 컴포넌트로 구현할 수 있도록 작업 범위, 파일 후보, 검증 기준을 정의한다. + +**Architecture:** Title Bar는 공통 `TitleBar` 레이아웃을 기반으로 Home/Default 래퍼를 제공하고, Tab-Text Bar는 최대 3개 메뉴와 단일 선택 상태를 관리하는 별도 컴포넌트로 분리한다. 신규 파일은 `SodaLive/Sources/V2/Component/**`에 둔다. + +**Tech Stack:** SwiftUI, existing `SodaTypography`, existing `Color` extension, existing asset catalog + +--- + +## 기준 문서 + +- PRD: `docs/prd/20260519_재사용타이틀탭텍스트바컴포넌트_PRD.md` +- 검증 가이드: `docs/agent-guides/build-test-verification.md` +- 코드 스타일: `docs/agent-guides/code-style.md` + +## 구현 대상 파일 후보 + +### 생성 + +- `SodaLive/Sources/V2/Component/TitleBar.swift`: 공통 Title Bar 레이아웃 +- `SodaLive/Sources/V2/Component/HomeTitleBar.swift`: `img_text_logo_v2` 기반 Home Title Bar 래퍼 +- `SodaLive/Sources/V2/Component/DefaultTitleBar.swift`: 화면명 기반 Default Title Bar 래퍼 +- `SodaLive/Sources/V2/Component/TextTabBar.swift`: 텍스트 탭 바 + +### 수정 후보 + +- `SodaLive.xcodeproj/project.pbxproj`: 신규 Swift 파일을 앱 타깃 Sources에 포함 + +### 수정하지 않음 + +- `SodaLive/Sources/NavigationBar/HomeNavigationBar.swift`: 기존 화면 의존 NavigationBar는 유지 +- `SodaLive/Sources/NavigationBar/DetailNavigationBar.swift`: 기존 상세 NavigationBar는 유지 +- `SodaLive/Resources/Assets.xcassets/**`: 신규 에셋 추가 없음 + +## 구현 체크리스트 + +### Task 1: 컴포넌트 위치 확인 + +**Files:** `SodaLive/Sources/V2/Component/**` + +- [x] 신규 디렉터리 `SodaLive/Sources/V2/Component`를 생성한다. +- [x] `SodaLive/Sources/V2/Common`은 UI 컴포넌트보다 공통 유틸리티/기반 타입으로 해석될 여지가 있으므로 사용하지 않는다. +- [x] QA: 선택한 위치가 `AGENTS.md`의 신규 코드 위치 규칙과 충돌하지 않는지 확인한다. + +### Task 2: Title Bar 공통 레이아웃 작성 + +**Files:** `TitleBar.swift` + +- [x] 높이 `60`, `maxWidth: .infinity`, `Color.black` 배경을 적용한다. +- [x] 좌측 슬롯, `Spacer()`, 우측 메뉴 슬롯으로 구성한다. +- [x] 모든 콘텐츠를 세로 가운데 정렬한다. +- [x] 우측 메뉴 슬롯은 호출부가 아이콘과 액션을 변경할 수 있게 한다. +- [x] 좌우 horizontal padding은 `20`을 적용한다. +- [x] QA: SwiftUI Preview 또는 실제 화면에서 높이 60과 좌우 배치가 유지되는지 확인한다. + +### Task 3: Home Title Bar 작성 + +**Files:** `HomeTitleBar.swift` + +- [x] 좌측에 `Image("img_text_logo_v2")`를 표시한다. +- [x] 공통 `TitleBar`에 우측 메뉴 슬롯을 전달한다. +- [x] 우측 메뉴 아이콘은 고정하지 않는다. +- [x] QA: 좌측 로고와 우측 메뉴가 같은 세로 중앙선에 정렬되는지 확인한다. + +### Task 4: Default Title Bar 작성 + +**Files:** `DefaultTitleBar.swift` + +- [x] 좌측에 호출부가 전달한 화면명을 표시한다. +- [x] 화면명 텍스트 스타일은 구현 시 Figma 기준과 기존 Typography 토큰을 대조해 최소 변경으로 적용한다. +- [x] 공통 `TitleBar`에 우측 메뉴 슬롯을 전달한다. +- [x] QA: 긴 화면명 입력 시 한 줄 표시와 우측 메뉴 영역 침범 여부를 확인한다. + +### Task 5: Tab-Text Bar 작성 + +**Files:** `TextTabBar.swift` + +- [x] 높이 `52`, `maxWidth: .infinity`, `Color.black` 배경을 적용한다. +- [x] 메뉴 수는 1개 이상 3개 이하를 정상 입력 조건으로 둔다. +- [x] 텍스트 스타일은 `.appFont(.heading3)`를 사용한다. +- [x] 일반 상태 색상은 `Color.gray600`, 선택 상태 색상은 `Color.white`를 사용한다. +- [x] 텍스트 메뉴는 왼쪽 정렬하고 메뉴 간 간격은 `20`을 적용한다. +- [x] 탭 선택 시 호출부 상태가 갱신되고 선택 상태가 하나만 유지되도록 API를 설계한다. +- [x] QA: 1개, 2개, 3개 메뉴에서 선택 상태가 하나만 표시되는지 확인한다. + +### Task 6: 프로젝트 연결 + +**Files:** `SodaLive.xcodeproj/project.pbxproj` + +- [x] 신규 Swift 파일을 필요한 앱 타깃 Sources에 포함한다. +- [x] QA: Xcode 프로젝트에서 신규 파일이 빌드 대상에 포함되는지 확인한다. + +### Task 7: 검증 + +**Files:** 신규/수정 Swift 파일 전체 + +- [x] `lsp_diagnostics`로 신규 Swift 파일의 정적 진단을 확인한다. +- [x] 가능한 경우 문서의 검증 가이드에 따라 앱 빌드 또는 관련 스킴 빌드를 실행한다. +- [x] SwiftUI Preview 또는 임시 호출부에서 `Home Title Bar`, `Default Title Bar`, `Tab-Text Bar`를 수동 확인한다. +- [x] QA: PRD의 Success Criteria를 항목별로 대조한다. + +## 구현 시 주의사항 + +- 이번 문서 생성 작업에서는 코드 구현을 하지 않는다. +- 우측 메뉴는 화면별로 다르므로 컴포넌트 내부에서 특정 메뉴 아이콘을 강제하지 않는다. +- `Tab-Text Bar`는 최대 3개 제약을 넘는 범용 스크롤 탭으로 확장하지 않는다. +- 기존 NavigationBar를 교체 적용하는 작업은 별도 요청 전까지 하지 않는다. +- 파일 생성 위치는 `SodaLive/Sources/V2/Component`로 고정한다. + +## 검증 기록 + +- 문서 작성 전 `SodaLive/Sources/NavigationBar/HomeNavigationBar.swift`, `DetailNavigationBar.swift`를 확인해 기존 상단 바 구현 패턴을 확인했다. +- 문서 작성 전 `SodaLive/Sources/Extensions/FontModifier.swift`를 확인해 `SodaTypography.heading3`와 `.appFont(_:)`를 확인했다. +- 문서 작성 전 `SodaLive/Sources/UI/Theme/Color.swift`를 확인해 `Color.gray600`, `Color.white`, `Color.black` 사용 가능성을 확인했다. +- 문서 작성 전 `SodaLive/Resources/Assets.xcassets/v2/img_text_logo_v2.imageset` 존재를 확인했다. +- `SodaLive/Sources/V2` 아래에는 현재 `Main`만 있고 전역 `Common`/`Component`는 없지만, 기존 UI 재사용 View는 `UI/Component` 및 도메인별 `.../V2/Component` 명명 관례를 사용하므로 `V2/Component`를 선택했다. +- 2026-05-19 구현 시 `SodaLive/Sources/V2/Component/TitleBar.swift`, `HomeTitleBar.swift`, `DefaultTitleBar.swift`, `TextTabBar.swift`를 생성했다. +- 2026-05-19 구현 시 `SodaLive.xcodeproj/project.pbxproj`에 신규 `Component` 그룹과 4개 Swift 파일을 `SodaLive`, `SodaLive-dev` 두 앱 타깃 Sources에 등록했다. +- 2026-05-19 테스트 타깃 확인: `XCTestCase`, `@testable import`, `*Tests` 디렉터리/파일 및 테스트 번들 타깃이 확인되지 않아 XCTest 기반 RED 단계를 추가하지 않았다. +- 2026-05-19 정적 진단: `lsp_diagnostics`는 신규 파일과 기존 `SodaLive/Sources/V2/Main/MainTabBarButton.swift` 모두에서 같은 형태의 SourceKit 프로젝트 컨텍스트 미해결 오류를 보고했다. 컴파일 가능 여부는 아래 `xcodebuild`로 최종 확인했다. +- 2026-05-19 빌드 검증: `xcodebuild -workspace 'SodaLive.xcworkspace' -scheme 'SodaLive-dev' -configuration Debug build` 실행 결과 `** BUILD SUCCEEDED **`. +- 2026-05-19 추가 빌드 검증: `xcodebuild -workspace 'SodaLive.xcworkspace' -scheme 'SodaLive' -configuration Debug build` 실행 결과 `** BUILD SUCCEEDED **`. Crashlytics dSYM 관련 경고와 일부 외부 모듈 dependency scan 경고가 출력됐으나 빌드는 성공했다. +- 2026-05-19 LSP 진단: `SodaLive/Sources/V2/Component/*.swift` 대상 SourceKit 진단에서 `SodaSpacing`, `appFont`, `Color.soda400`, `TitleBar` 등 프로젝트 문맥 미해결 오류가 출력됐다. 동일 파일들이 `SodaLive-dev`, `SodaLive` 양쪽 Debug 빌드에서 컴파일 성공했으므로 단일 파일 SourceKit 문맥 이슈로 기록한다. +- 2026-05-19 수동 QA 대체 검증: 신규 컴포넌트는 직접 화면 적용 범위가 아니므로 Xcode 프로젝트 등록 확인(`TitleBar.swift`, `HomeTitleBar.swift`, `DefaultTitleBar.swift`, `TextTabBar.swift`가 `PBXFileReference`, `PBXBuildFile`, 양쪽 `PBXSourcesBuildPhase`에 존재)과 두 스킴 빌드 성공으로 사용 가능성을 확인했다. +- 2026-05-19 추가 수정: `TitleBar` horizontal padding을 `20`으로 변경하고, `TextTabBar`를 왼쪽 정렬 및 텍스트 간격 `20`으로 변경했다. +- 2026-05-19 네이밍 정책: 탭 바 컴포넌트는 `CapsuleTabBar`, `TextTabBar`처럼 `스타일 + TabBar` 형식으로 통일한다. +- 2026-05-19 추가 수정: 이번에 추가한 컴포넌트의 하드코딩 값 중 `black`/`white` 컬러는 제외하고, 변경 가능한 spacing `20`만 `SodaLive/Sources/UI/Theme/**`의 기존 토큰 `SodaSpacing.s20`으로 변경했다. `Color.gray600`은 이미 테마 토큰을 사용 중이며, 이번 컴포넌트에는 `Radius` 사용이 없어 변경 대상이 없었다. +- 2026-05-19 추가 구현: Figma node `20:3590` 기준 캡슐형 탭 바를 `SodaLive/Sources/V2/Component/CapsuleTabBar.swift`로 추가했다. 항목 수 제한 없이 `ScrollView(.horizontal, showsIndicators: false)`와 `LazyHStack`으로 가로 스크롤되도록 구현했고, 선택 상태는 외부 `Binding`으로 관리한다. +- 2026-05-19 추가 구현: `SodaLive.xcodeproj/project.pbxproj`에 `CapsuleTabBar.swift`를 `SodaLive`, `SodaLive-dev` 두 앱 타깃 Sources에 등록했다. +- 2026-05-19 빌드 검증: `xcodebuild -workspace 'SodaLive.xcworkspace' -scheme 'SodaLive-dev' -configuration Debug build` 실행 결과 `** BUILD SUCCEEDED **`. diff --git a/docs/prd/20260519_재사용타이틀탭텍스트바컴포넌트_PRD.md b/docs/prd/20260519_재사용타이틀탭텍스트바컴포넌트_PRD.md new file mode 100644 index 0000000..53138c0 --- /dev/null +++ b/docs/prd/20260519_재사용타이틀탭텍스트바컴포넌트_PRD.md @@ -0,0 +1,145 @@ +# PRD: 재사용 타이틀 바 및 탭 텍스트 바 컴포넌트 + +## 1. Overview + +화면 상단에서 공통으로 사용할 `Title Bar` 계열 컴포넌트와 텍스트 기반 탭 전환에 사용할 `Tab-Text Bar` 컴포넌트를 SwiftUI 재사용 컴포넌트로 정의한다. 구현 대상은 신규 코드 위치 규칙에 따라 `SodaLive/Sources/V2/Component/**` 아래에 배치할 재사용 UI 단위다. + +--- + +## 2. Background + +현재 프로젝트에는 기존 `SodaLive/Sources/NavigationBar/HomeNavigationBar.swift`, `SodaLive/Sources/NavigationBar/DetailNavigationBar.swift`가 있으나 높이, 로고, 우측 메뉴 슬롯, V2 디자인 토큰 기준이 이번 요구사항과 다르다. + +최근 코드에는 다음 기반 요소가 이미 존재한다. + +- `SodaLive/Sources/Extensions/FontModifier.swift`: `SodaTypography.heading3` 및 `.appFont(_:)` 제공 +- `SodaLive/Sources/UI/Theme/Color.swift`: `Color.gray600`, `Color.white`, `Color.black` 사용 가능 +- `SodaLive/Resources/Assets.xcassets/v2/img_text_logo_v2.imageset`: 홈 타이틀 바 로고 에셋 존재 +- `SodaLive/Resources/Assets.xcassets/v2/ic_bar_search.imageset`, `ic_bar_bell.imageset`, `ic_bar_cash.imageset`: 화면별로 교체 가능한 우측 메뉴 아이콘 후보 존재 + +--- + +## 3. Goals + +- `Home Title Bar`를 재사용 컴포넌트로 제공한다. +- `Default Title Bar`를 재사용 컴포넌트로 제공한다. +- `Tab-Text Bar`를 재사용 컴포넌트로 제공한다. +- 각 컴포넌트는 Figma 요구사항의 높이, 배경색, 세로 가운데 정렬, 선택 색상 규칙을 따른다. +- 화면별 우측 메뉴 아이콘과 액션은 호출부에서 변경할 수 있게 한다. +- `Tab-Text Bar`는 최대 3개 메뉴와 단일 선택 상태를 보장한다. + +--- + +## 4. Non-Goals + +- 기존 `HomeNavigationBar`, `DetailNavigationBar`를 삭제하거나 대규모 리팩터링하지 않는다. +- 실제 화면에 컴포넌트를 적용하는 작업은 이 범위에 포함하지 않는다. +- 신규 이미지 에셋 제작은 하지 않는다. +- 탭별 콘텐츠 화면이나 비즈니스 로직은 구현하지 않는다. +- Figma Code Connect 매핑 작업은 하지 않는다. + +--- + +## 5. Core Requirements + +### 5.1 Title Bar 공통 규칙 + +- Width: full +- Height: `60` +- Alignment: 세로 가운데 정렬 +- Background: `Color.black` +- 좌측 콘텐츠, 빈 영역, 우측 메뉴 영역의 `HStack` 구조를 사용한다. +- 우측 메뉴 아이콘은 사용하는 화면마다 다르므로 호출부에서 주입 가능해야 한다. + +### 5.2 Home Title Bar + +- Figma: `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=20-3575&m=dev` +- 좌측에는 `img_text_logo_v2` 이미지를 표시한다. +- 중앙은 `Spacer()`로 빈 영역을 채운다. +- 우측에는 호출부가 제공하는 메뉴 아이콘 또는 메뉴 View를 표시한다. + +### 5.3 Default Title Bar + +- Figma: `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=20-3576&m=dev` +- 좌측에는 화면명을 표시한다. +- 화면명은 호출부에서 변경 가능해야 한다. +- 중앙은 `Spacer()`로 빈 영역을 채운다. +- 우측에는 호출부가 제공하는 메뉴 아이콘 또는 메뉴 View를 표시한다. + +### 5.4 Tab-Text Bar + +- Figma: `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=20-3585&m=dev` +- Width: full +- Height: `52` +- Alignment: 세로 가운데 정렬 +- Background: `Color.black` +- Typography: `SodaTypography.heading3` +- Normal Color: `Color.gray600` +- Selected Color: `Color.white` +- 메뉴 선택 시 해당 메뉴만 Selected 상태가 된다. +- Selected 상태는 반드시 하나만 존재해야 한다. +- 메뉴 개수는 최대 3개까지만 허용한다. + +--- + +## 6. Recommended Approach + +### 접근안 A: Title Bar와 Tab-Text Bar를 각각 독립 컴포넌트로 작성 + +`HomeTitleBar`, `DefaultTitleBar`, `TextTabBar`를 분리한다. 호출부에서 사용법이 명확하고 각 컴포넌트의 요구사항을 그대로 표현하기 쉽다. 단점은 Title Bar의 공통 레이아웃이 중복될 수 있다. + +### 접근안 B: 공통 `TitleBar` 기반에 Home/Default 편의 생성자를 제공 + +하나의 공통 `TitleBar`가 좌측 콘텐츠와 우측 메뉴 슬롯을 받고, `HomeTitleBar`와 `DefaultTitleBar`는 얇은 래퍼로 제공한다. 중복이 적고 우측 메뉴 커스터마이징이 쉽다. 단점은 최초 구현 시 구조가 접근안 A보다 조금 더 추상적이다. + +### 접근안 C: 모든 영역을 ViewBuilder 슬롯으로만 받는 범용 Bar 작성 + +가장 유연하지만 현재 요구사항보다 범위가 넓다. 화면마다 다른 배치가 가능해지는 대신 디자인 규칙이 느슨해질 수 있다. + +### 권장 + +접근안 B를 권장한다. Title Bar의 공통 규칙은 한 곳에서 유지하고, Home/Default 사용처는 명확한 이름의 래퍼로 제공한다. `TextTabBar`는 선택 상태와 최대 3개 제약을 자체적으로 갖는 별도 컴포넌트로 둔다. + +--- + +## 7. Technical Constraints + +- SwiftUI 기반으로 작성한다. +- 신규 파일은 구현 시 `SodaLive/Sources/V2/Component/**` 아래에 둔다. +- `SodaLive/Sources/V2/Common/**`은 UI 컴포넌트보다 공통 유틸리티/기반 타입으로 해석될 여지가 있으므로 이번 컴포넌트 위치로 사용하지 않는다. +- 색상은 기존 `Color.black`, `Color.gray600`, `Color.white`를 우선 사용한다. +- 폰트는 기존 `.appFont(.heading3)` 사용을 우선한다. +- `TextTabBar`는 호출부가 선택 상태를 소유할 수 있도록 `Binding` 또는 선택 콜백 기반 API를 사용한다. +- 메뉴가 0개이거나 4개 이상인 경우는 구현 단계에서 안전한 실패 방식이 필요하다. 단순 구현을 우선해 1...3개 입력만 정상 사용 조건으로 문서화한다. + +--- + +## 8. Success Criteria + +- 문서 기준 구현 후 `Home Title Bar`는 높이 60, 검정 배경, 좌측 `img_text_logo_v2`, 우측 교체 가능한 메뉴 아이콘을 표시한다. +- 문서 기준 구현 후 `Default Title Bar`는 높이 60, 검정 배경, 좌측 화면명, 우측 교체 가능한 메뉴 아이콘을 표시한다. +- 문서 기준 구현 후 `Tab-Text Bar`는 높이 52, 검정 배경, 최대 3개 텍스트 메뉴를 표시한다. +- `Tab-Text Bar`의 일반 메뉴는 `Color.gray600`, 선택 메뉴는 `Color.white`로 표시된다. +- `Tab-Text Bar`의 텍스트는 `SodaTypography.heading3`를 사용한다. +- 탭 선택 시 선택 상태는 항상 하나만 유지된다. + +--- + +## 9. Decisions + +- 문서 단계에서는 코드 구현을 하지 않는다. +- Figma에서 Code Connect 매핑을 요구했지만, 이번 요청 범위가 문서 생성으로 축소되었으므로 매핑 작업은 제외한다. +- `img_text_logo_v2`는 기존 v2 에셋을 사용한다. +- 우측 메뉴는 특정 아이콘명으로 고정하지 않고 호출부 주입 방식으로 정의한다. +- 파일 생성 위치는 `SodaLive/Sources/V2/Component/**`로 결정한다. + +--- + +## 10. Verification Notes + +- `SodaLive/Sources/NavigationBar/HomeNavigationBar.swift`, `DetailNavigationBar.swift`를 확인해 기존 NavigationBar 패턴과 차이를 확인했다. +- `SodaLive/Sources/Extensions/FontModifier.swift`를 확인해 `SodaTypography.heading3`와 `.appFont(_:)` 사용 가능성을 확인했다. +- `SodaLive/Sources/UI/Theme/Color.swift`를 확인해 `Color.gray600` 토큰 값을 확인했다. +- `SodaLive/Resources/Assets.xcassets/v2/img_text_logo_v2.imageset`가 존재함을 확인했다. +- `SodaLive/Sources/V2` 아래에는 현재 `Main`만 있고 전역 `Common`/`Component`는 없지만, 기존 UI 재사용 View는 `UI/Component` 및 도메인별 `.../V2/Component` 명명 관례를 사용하므로 `V2/Component`를 선택했다. +- 이번 작업은 문서 생성만 수행하며 Swift 소스 변경은 하지 않는다.