feat(widget): 공통 탭바와 타이틀바 컴포넌트를 추가한다

This commit is contained in:
2026-05-19 20:29:42 +09:00
parent 3121d9dca9
commit 4457941193
21 changed files with 1360 additions and 0 deletions

View File

@@ -0,0 +1,401 @@
# Capsule Tab Bar 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:** Figma `20:3590` 기준으로 메뉴가 많으면 가로 스크롤되는 재사용 가능한 Capsule Tab Bar Component를 추가한다.
**Architecture:** 기존 `TextTabBarView`는 text 타입 탭 전용으로 유지하고, capsule 타입은 `CapsuleTabBarView`로 분리한다. XML 레이아웃은 `HorizontalScrollView`와 내부 `LinearLayout`을 제공하고, Kotlin custom view가 메뉴 TextView를 동적으로 생성해 selected 상태를 단일 관리한다.
**Tech Stack:** Android XML Views, Kotlin custom View, ViewBinding/resource merge, JUnit4 local unit test.
---
## 작업 목표
- Figma `20:3590` capsule 탭바 디자인을 Android XML/Kotlin 재사용 컴포넌트로 구현한다.
- 메뉴 개수가 많으면 가로 스크롤되어야 한다.
- 기존 `TextTabBarView`와 기존 화면은 변경하지 않는다.
## 파일 구조
- Create: `app/src/main/res/layout/view_capsule_tab_bar.xml`
- `CapsuleTabBarView` 루트, 내부 `HorizontalScrollView`, 내부 `LinearLayout` 컨테이너를 정의한다.
- Create: `app/src/main/res/drawable/bg_capsule_tab_selected.xml`
- selected capsule 배경을 정의한다.
- Create: `app/src/main/res/drawable/bg_capsule_tab_normal.xml`
- normal capsule 배경과 border를 정의한다.
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/CapsuleTabSelectionState.kt`
- 메뉴 목록과 selected index를 검증/보정하는 순수 Kotlin 상태 객체를 정의한다.
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/CapsuleTabBarView.kt`
- 메뉴 TextView를 동적으로 생성하고, 선택 상태와 콜백을 관리한다.
- Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/CapsuleTabSelectionStateTest.kt`
- 1개 이상 메뉴, out-of-range 보정, 단일 selected 상태를 검증한다.
- Modify: `docs/plan-task/20260519_캡슐탭바컴포넌트.md`
- 구현 중 체크박스와 검증 기록을 누적한다.
## 구현 계획
### Task 1: Capsule tab 선택 상태 TDD
**Files:**
- Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/CapsuleTabSelectionStateTest.kt`
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/CapsuleTabSelectionState.kt`
- [x] **Step 1: RED - 선택 상태 테스트 추가**
```kotlin
package kr.co.vividnext.sodalive.v2.widget
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Test
class CapsuleTabSelectionStateTest {
@Test
fun `create keeps one selected tab with one or more menus`() {
val state = CapsuleTabSelectionState.create(listOf("추천", "랭킹", "팔로잉"), selectedIndex = 2)
assertEquals(listOf("추천", "랭킹", "팔로잉"), state.menus)
assertEquals(2, state.selectedIndex)
assertFalse(state.isSelected(0))
assertFalse(state.isSelected(1))
assertTrue(state.isSelected(2))
}
@Test
fun `create rejects empty menus`() {
assertThrows(IllegalArgumentException::class.java) {
CapsuleTabSelectionState.create(emptyList())
}
}
@Test
fun `create normalizes out of range selected index to first menu`() {
val state = CapsuleTabSelectionState.create(listOf("추천", "랭킹"), selectedIndex = 5)
assertEquals(0, state.selectedIndex)
assertTrue(state.isSelected(0))
assertFalse(state.isSelected(1))
}
@Test
fun `select changes selected tab`() {
val state = CapsuleTabSelectionState.create(listOf("추천", "랭킹", "팔로잉"))
.select(1)
assertEquals(1, state.selectedIndex)
assertFalse(state.isSelected(0))
assertTrue(state.isSelected(1))
assertFalse(state.isSelected(2))
}
}
```
- [x] **Step 2: RED 실행**
Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.CapsuleTabSelectionStateTest"`
Expected: `Unresolved reference 'CapsuleTabSelectionState'`로 실패한다.
- [x] **Step 3: GREEN - 최소 상태 객체 추가**
```kotlin
package kr.co.vividnext.sodalive.v2.widget
class CapsuleTabSelectionState private constructor(
val menus: List<String>,
val selectedIndex: Int
) {
fun isSelected(index: Int): Boolean = selectedIndex == index
fun select(index: Int): CapsuleTabSelectionState = create(menus, index)
companion object {
fun create(
menus: List<String>,
selectedIndex: Int = 0
): CapsuleTabSelectionState {
require(menus.isNotEmpty()) { "Capsule tab bar requires at least one menu." }
val normalizedSelectedIndex = if (selectedIndex in menus.indices) selectedIndex else 0
return CapsuleTabSelectionState(
menus = menus.toList(),
selectedIndex = normalizedSelectedIndex
)
}
}
}
```
- [x] **Step 4: GREEN 실행**
Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.CapsuleTabSelectionStateTest"`
Expected: `BUILD SUCCESSFUL`
### Task 2: Capsule tab XML 리소스 추가
**Files:**
- Create: `app/src/main/res/drawable/bg_capsule_tab_selected.xml`
- Create: `app/src/main/res/drawable/bg_capsule_tab_normal.xml`
- Create: `app/src/main/res/layout/view_capsule_tab_bar.xml`
- [x] **Step 1: selected capsule drawable 추가**
`app/src/main/res/drawable/bg_capsule_tab_selected.xml`
```xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/soda_400" />
<corners android:radius="100dp" />
</shape>
```
- [x] **Step 2: normal capsule drawable 추가**
`app/src/main/res/drawable/bg_capsule_tab_normal.xml`
```xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/black" />
<stroke
android:width="1dp"
android:color="@color/gray_700" />
<corners android:radius="100dp" />
</shape>
```
- [x] **Step 3: capsule tab bar layout 추가**
`app/src/main/res/layout/view_capsule_tab_bar.xml`
```xml
<?xml version="1.0" encoding="utf-8"?>
<kr.co.vividnext.sodalive.v2.widget.CapsuleTabBarView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="52dp"
android:background="@color/black">
<HorizontalScrollView
android:id="@+id/hsv_capsule_tab_bar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="false"
android:overScrollMode="never"
android:scrollbars="none">
<LinearLayout
android:id="@+id/ll_capsule_tab_container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/spacing_20" />
</HorizontalScrollView>
</kr.co.vividnext.sodalive.v2.widget.CapsuleTabBarView>
```
### Task 3: CapsuleTabBarView 구현
**Files:**
- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/CapsuleTabBarView.kt`
- [x] **Step 1: custom view 추가**
```kotlin
package kr.co.vividnext.sodalive.v2.widget
import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import kr.co.vividnext.sodalive.R
class CapsuleTabBarView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private var container: LinearLayout? = null
private var selectionState: CapsuleTabSelectionState? = null
private var onTabSelected: ((index: Int) -> Unit)? = null
override fun onFinishInflate() {
super.onFinishInflate()
container = findViewById(R.id.ll_capsule_tab_container)
}
fun setMenus(
menus: List<String>,
selectedIndex: Int = 0
) {
applyState(CapsuleTabSelectionState.create(menus, selectedIndex), notifySelection = false)
}
fun selectTab(index: Int) {
val currentState = selectionState ?: return
if (index !in currentState.menus.indices) return
if (index == currentState.selectedIndex) return
applyState(currentState.select(index), notifySelection = true)
}
fun setOnTabSelectedListener(listener: ((index: Int) -> Unit)?) {
onTabSelected = listener
}
private fun applyState(
state: CapsuleTabSelectionState,
notifySelection: Boolean
) {
selectionState = state
val tabContainer = requireNotNull(container) { "Capsule tab container is not inflated." }
tabContainer.removeAllViews()
state.menus.forEachIndexed { index, label ->
tabContainer.addView(createTabView(label, state.isSelected(index), index))
}
if (notifySelection) {
onTabSelected?.invoke(state.selectedIndex)
}
}
private fun createTabView(
label: String,
selected: Boolean,
index: Int
): TextView {
return TextView(context).apply {
text = label
isSelected = selected
setTextAppearance(R.style.Typography_Body5)
setTextColor(ContextCompat.getColor(context, R.color.white))
background = ContextCompat.getDrawable(
context,
if (selected) R.drawable.bg_capsule_tab_selected else R.drawable.bg_capsule_tab_normal
)
gravity = android.view.Gravity.CENTER
minHeight = resources.getDimensionPixelSize(R.dimen.spacing_32)
setPadding(
resources.getDimensionPixelSize(R.dimen.spacing_12),
resources.getDimensionPixelSize(R.dimen.spacing_8),
resources.getDimensionPixelSize(R.dimen.spacing_12),
resources.getDimensionPixelSize(R.dimen.spacing_8)
)
setOnClickListener { selectTab(index) }
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
resources.getDimensionPixelSize(R.dimen.spacing_32)
).apply {
if (index > 0) {
marginStart = resources.getDimensionPixelSize(R.dimen.spacing_8)
}
}
}
}
}
```
- [x] **Step 2: 리소스 참조 확인**
Run: `rg -n "CapsuleTabBarView|ll_capsule_tab_container|bg_capsule_tab|Typography_Body5|spacing_8|spacing_12|spacing_32" app/src/main/res app/src/main/java/kr/co/vividnext/sodalive/v2/widget`
Expected: 새 layout/drawable/Kotlin 파일의 참조가 모두 조회된다.
### Task 4: 검증 및 문서 기록
**Files:**
- Modify: `docs/plan-task/20260519_캡슐탭바컴포넌트.md`
- [x] **Step 1: LSP 진단 실행**
Run: `lsp_diagnostics` on modified Kotlin/XML files
Expected: 새 오류가 없다. Kotlin/XML LSP가 환경에 없으면 검증 기록에 남긴다.
- [x] **Step 2: 단위 테스트 실행**
Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.CapsuleTabSelectionStateTest"`
Expected: `BUILD SUCCESSFUL`
- [x] **Step 3: 리소스 빌드 실행**
Run: `./gradlew :app:assembleDebug`
Expected: `BUILD SUCCESSFUL`
- [x] **Step 4: 스타일 검사 실행**
Run: `./gradlew :app:ktlintCheck`
Expected: `BUILD SUCCESSFUL`
- [x] **Step 5: 검증 기록 누적**
문서 하단 `검증 기록`에 실행한 명령, 결과, 빌드 성공 여부를 한국어로 기록한다.
## 체크리스트
- [x] AC1: `view_capsule_tab_bar.xml`은 height `52dp`, background black을 가진다.
- QA: XML 속성 확인
- [x] AC2: `view_capsule_tab_bar.xml``HorizontalScrollView`를 포함해 메뉴 초과 시 가로 스크롤 가능하다.
- QA: XML 구조와 `assembleDebug` 확인
- [x] AC3: selected 탭은 `soda_400` 배경과 white 텍스트를 사용한다.
- QA: `bg_capsule_tab_selected.xml`, `CapsuleTabBarView.kt` 확인
- [x] AC4: normal 탭은 black 배경, `gray_700` border, white 텍스트를 사용한다.
- QA: `bg_capsule_tab_normal.xml`, `CapsuleTabBarView.kt` 확인
- [x] AC5: 메뉴 1개 이상을 허용하고 항상 하나만 selected 상태다.
- QA: `CapsuleTabSelectionStateTest`
- [x] AC6: 기존 `TextTabBarView`와 기존 화면은 변경하지 않는다.
- QA: 변경 파일 목록 확인
- [x] AC7: 리소스 병합, 단위 테스트, ktlint 검증이 성공한다.
- QA: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.CapsuleTabSelectionStateTest"`, `./gradlew :app:assembleDebug`, `./gradlew :app:ktlintCheck`
## 검증 기록
- 2026-05-19
- 무엇/왜/어떻게: Figma `20:3590` 디자인을 확인하고, 기존 Text Tab과 분리된 Capsule Tab Bar PRD 및 구현 계획/TASK 문서를 작성했다.
- 실행 명령/도구:
- `Figma_get_design_context(20:3590)`
- `Figma_get_screenshot(20:3590)`
- `read(app/src/main/res/layout/view_text_tab_bar.xml)`
- `read(app/src/main/java/kr/co/vividnext/sodalive/v2/widget/TextTabBarView.kt)`
- `read(app/src/main/res/values/colors.xml)`
- `read(app/src/main/res/values/dimens.xml)`
- `read(app/src/main/res/values/typography.xml)`
- 결과:
- PRD 문서는 `docs/prd/20260519_캡슐탭바컴포넌트_prd.md`에 작성했다.
- 계획/TASK 문서는 `docs/plan-task/20260519_캡슐탭바컴포넌트.md`에 작성했다.
- 구현은 아직 수행하지 않았다.
- 2026-05-19
- 무엇/왜/어떻게: 탭바 컴포넌트 네이밍을 `{Type}TabBarView` 방식으로 통일하기로 결정해 문서의 기존 text 탭 컴포넌트 참조를 `TextTabBarView` 계열로 갱신했다.
- 실행 명령/도구:
- `rg -n "CapsuleTabBarView|TabTextBarView|TabText|TextTab" app/src docs`
- `apply_patch(docs/prd/20260519_캡슐탭바컴포넌트_prd.md)`
- `apply_patch(docs/plan-task/20260519_캡슐탭바컴포넌트.md)`
- 결과:
- Capsule 컴포넌트는 `CapsuleTabBarView` 이름을 유지한다.
- 기존 text 탭 컴포넌트 참조는 `TextTabBarView`, `TextTabSelectionState` 기준으로 정리했다.
- 2026-05-19
- 무엇/왜/어떻게: 계획 문서에 따라 `CapsuleTabSelectionState`를 TDD로 추가하고, `HorizontalScrollView` 기반 재사용 `CapsuleTabBarView`와 capsule 배경 리소스를 구현했다.
- 실행 명령/도구:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.CapsuleTabSelectionStateTest"` (RED)
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.CapsuleTabSelectionStateTest"` (GREEN)
- `rg -n "CapsuleTabBarView|ll_capsule_tab_container|bg_capsule_tab|Typography_Body5|spacing_8|spacing_12|spacing_32" app/src/main/res app/src/main/java/kr/co/vividnext/sodalive/v2/widget`
- `lsp_diagnostics` on modified Kotlin/XML files
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.CapsuleTabSelectionStateTest"`
- `./gradlew :app:assembleDebug`
- `./gradlew :app:ktlintCheck`
- 결과:
- RED 테스트는 `Unresolved reference 'CapsuleTabSelectionState'`로 실패해 신규 상태 객체 부재를 확인했다.
- GREEN 테스트는 `BUILD SUCCESSFUL`로 통과했다.
- 리소스 참조 확인에서 `CapsuleTabBarView`, `ll_capsule_tab_container`, `bg_capsule_tab_*`, `Typography_Body5`, `spacing_8`, `spacing_12`, `spacing_32` 참조를 확인했다.
- Kotlin/XML LSP는 현재 환경에 서버가 없어 `No LSP server configured for extension: .kt/.xml`로 실행 불가했다.
- 단위 테스트, `assembleDebug`, `ktlintCheck`는 모두 `BUILD SUCCESSFUL`로 종료했다.

View File

@@ -0,0 +1,258 @@
# 타이틀바 및 탭 텍스트바 컴포넌트
## 작업 목표
- XML 레이아웃 기반 재사용 가능한 Title Bar Component를 개발한다.
- XML 레이아웃 기반 재사용 가능한 Tab-Text Bar Component를 개발한다.
- 구현 범위는 컴포넌트 추가로 한정하고 기존 화면 일괄 적용은 제외한다.
## 구현 계획
### Task 1: 기존 리소스 및 유사 UI 확인
**Files:**
- Read: `app/src/main/res/layout/toolbar_audio_content_main.xml`
- Read: `app/src/main/res/layout/activity_audio_content_main.xml`
- Read: `app/src/main/res/layout/activity_can_status.xml`
- Read: `app/src/main/res/values/colors.xml`
- Read: `app/src/main/res/values/typography.xml`
- [x] **Step 1: 기존 타이틀 바 패턴 확인**
Run: `rg -n "toolbar|Toolbar|title|Title" app/src/main/res/layout app/src/main/java/kr/co/vividnext/sodalive`
Expected: 기존 화면에서 include 또는 개별 toolbar 사용 패턴을 확인한다.
- [x] **Step 2: 기존 탭 바 패턴 확인**
Run: `rg -n "TabLayout|tabText|ContentMainTabText|color_tabbar" app/src/main/res app/src/main/java/kr/co/vividnext/sodalive`
Expected: 기존 TabLayout 및 탭 텍스트 스타일 사용 패턴을 확인한다.
- [x] **Step 3: 필수 리소스 확인**
Run: `rg -n "img_text_logo_v2|gray_600|Typography\.Heading3|<color name=\"white\"|<color name=\"black\"" app/src/main/res`
Expected: `gray_600`, `Typography.Heading3`, black/white 색상과 로고 drawable 존재 여부를 확인한다.
### Task 2: Title Bar Component 추가
**Files:**
- Create: `app/src/main/res/layout/view_title_bar_home.xml`
- Create: `app/src/main/res/layout/view_title_bar_default.xml`
- [x] **Step 1: Home Title Bar XML 추가**
`view_title_bar_home.xml`에 높이 60dp, 전체 너비, 검정 배경, 세로 중앙 정렬을 갖는 레이아웃을 추가한다. 좌측에는 `img_text_logo_v2`, 중앙에는 가변 빈 영역, 우측에는 교체 가능한 메뉴 아이콘 `ImageView`를 둔다.
- [x] **Step 2: Default Title Bar XML 추가**
`view_title_bar_default.xml`에 높이 60dp, 전체 너비, 검정 배경, 세로 중앙 정렬을 갖는 레이아웃을 추가한다. 좌측에는 화면명 `TextView`, 중앙에는 가변 빈 영역, 우측에는 교체 가능한 메뉴 아이콘 `ImageView`를 둔다.
- [x] **Step 3: 호출부 커스터마이징 지점 명확화**
각 레이아웃의 화면명 TextView와 메뉴 ImageView에는 ViewBinding에서 접근 가능한 명확한 id를 부여한다.
### Task 3: Tab-Text Bar Component 추가
**Files:**
- Create: `app/src/main/res/layout/view_text_tab_bar.xml`
- Create or Modify: 필요한 경우 탭 텍스트 색상 selector 리소스
- [x] **Step 1: Tab-Text Bar XML 추가**
`view_text_tab_bar.xml`에 높이 52dp, 전체 너비, 검정 배경, 세로 중앙 정렬을 갖는 텍스트 탭 바 레이아웃을 추가한다.
- [x] **Step 2: Typography와 색상 적용**
각 탭 텍스트는 `Typography.Heading3`을 적용하고, selected는 white, normal은 `gray_600`으로 표시되도록 color selector 또는 selected 상태 처리를 구성한다.
- [x] **Step 3: 메뉴 개수 및 단일 선택 상태 처리 방식 구현**
Kotlin helper 또는 커스텀 View를 추가하는 경우 메뉴 개수는 1~3개만 허용하고, 선택 변경 시 반드시 하나의 메뉴만 selected가 되도록 구현한다. XML include만으로 충분하지 않으면 최소 범위의 Kotlin 클래스를 추가한다.
### Task 4: 검증 및 문서 기록
**Files:**
- Modify: `docs/plan-task/20260519_타이틀바및탭텍스트바컴포넌트.md`
- [x] **Step 1: XML 및 Kotlin 진단 실행**
Run: `lsp_diagnostics` on modified XML/Kotlin files
Expected: 새 오류가 없다. XML LSP가 환경에 없으면 그 사실을 검증 기록에 남긴다.
- [x] **Step 2: 디버그 빌드 실행**
Run: `./gradlew :app:assembleDebug`
Expected: `BUILD SUCCESSFUL`
- [x] **Step 3: 수동 QA 수행**
컴포넌트를 임시 또는 대상 화면에서 inflate 가능한지 확인한다. 실제 화면 적용이 이번 범위에 포함되지 않으면 리소스 빌드 성공과 ViewBinding 접근 id 생성 여부를 확인한다.
- [x] **Step 4: 검증 기록 누적**
문서 하단 `검증 기록`에 실행한 명령, 결과, 빌드 성공 여부를 한국어로 기록한다.
## 체크리스트
- [x] AC1: Home Title Bar는 width `match_parent`, height `60dp`, black background, vertical center alignment를 가진다.
- QA: `view_title_bar_home.xml` 속성 확인
- [x] AC2: Home Title Bar는 좌측 `img_text_logo_v2`, 중앙 가변 빈 영역, 우측 메뉴 아이콘 구조를 가진다.
- QA: `view_title_bar_home.xml` 구조 및 id 확인
- [x] AC3: Default Title Bar는 width `match_parent`, height `60dp`, black background, vertical center alignment를 가진다.
- QA: `view_title_bar_default.xml` 속성 확인
- [x] AC4: Default Title Bar는 화면명 텍스트와 우측 메뉴 아이콘을 호출 화면에서 변경할 수 있다.
- QA: ViewBinding 접근 가능한 id 확인
- [x] AC5: Tab-Text Bar는 width `match_parent`, height `52dp`, black background, vertical center alignment를 가진다.
- QA: `view_text_tab_bar.xml` 속성 확인
- [x] AC6: Tab-Text Bar 텍스트는 `Typography.Heading3`, normal `gray_600`, selected white를 사용한다.
- QA: XML style/color selector 또는 Kotlin 상태 처리 확인
- [x] AC7: Tab-Text Bar 메뉴 개수는 최대 3개이며, 항상 하나만 selected 상태다.
- QA: 선택 상태 변경 로직 또는 사용 계약 확인
- [x] AC8: 기존 화면에는 요청 없이 일괄 적용하지 않는다.
- QA: 변경 파일 목록에서 기존 화면 레이아웃 변경 여부 확인
- [x] AC9: 리소스 병합 및 디버그 빌드가 성공한다.
- QA: `./gradlew :app:assembleDebug`
## 검증 기록
- 2026-05-19
- 무엇/왜/어떻게: 사용자 요청에 따라 구현 없이 PRD와 구현 계획/TASK 문서만 작성했다. 기존 프로젝트가 XML Views + ViewBinding 중심이며, 구현 전 문서가 필요하다는 저장소 규칙을 반영했다.
- 실행 명령/도구:
- `read(docs/prd/sample-prd.md)`
- `read(docs/agent-guides/workflow-docs-commits.md)`
- `read(docs/agent-guides/code-style.md)`
- `read(docs/agent-guides/build-test-style.md)`
- `read(docs/prd/20260515_design_token_xml_resource_prd.md)`
- `read(docs/prd/20260515_typography_xml_style_prd.md)`
- `explore` agent result for existing UI patterns
- `Figma_get_design_context(20:3575, 20:3576, 20:3585)`
- 결과:
- PRD 문서는 `docs/prd/20260519_타이틀바및탭텍스트바컴포넌트_prd.md`에 작성했다.
- 계획/TASK 문서는 `docs/plan-task/20260519_타이틀바및탭텍스트바컴포넌트.md`에 작성했다.
- Figma 조회는 타임아웃되어 제공된 텍스트 요구사항을 기준으로 문서화했다.
- 코드, 리소스, 레이아웃 구현 파일은 변경하지 않았다.
- 무엇/왜/어떻게: 계획에 따라 재사용 가능한 XML Title Bar 2종, Text Tab Bar, 전용 텍스트 색상 selector, `TextTabSelectionState` 최소 상태 helper와 이를 사용하는 `TextTabBarView`를 추가했다. 기존 화면에는 적용하지 않았다.
- 실행 명령/도구:
- `read(docs/plan-task/20260519_타이틀바및탭텍스트바컴포넌트.md)`
- `read(app/src/main/res/layout/toolbar_audio_content_main.xml)`
- `read(app/src/main/res/layout/activity_audio_content_main.xml)`
- `read(app/src/main/res/layout/activity_can_status.xml)`
- `read(app/src/main/res/values/colors.xml)`
- `read(app/src/main/res/values/typography.xml)`
- `rg -n "toolbar|Toolbar|title|Title" app/src/main/res/layout app/src/main/java/kr/co/vividnext/sodalive`
- `rg -n "TabLayout|tabText|ContentMainTabText|color_tabbar" app/src/main/res app/src/main/java/kr/co/vividnext/sodalive`
- `rg -n "img_text_logo_v2|gray_600|Typography\.Heading3|<color name=\"white\"|<color name=\"black\"" app/src/main/res`
- `lsp_diagnostics` on modified XML/Kotlin files
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.TextTabSelectionStateTest"`
- `./gradlew :app:assembleDebug`
- 결과:
- Kotlin/XML LSP 서버가 현재 환경에 설정되어 있지 않아 `lsp_diagnostics`는 실행 불가했다.
- `TextTabSelectionStateTest`는 성공했다.
- `:app:assembleDebug``BUILD SUCCESSFUL`로 완료되어 리소스 병합과 ViewBinding 생성 가능성을 확인했다.
- Gradle deprecation 안내는 기존 빌드 환경 메시지로 남아 있다.
- 2026-05-19
- 무엇/왜/어떻게: 구현 결과를 재검토한 뒤, Text Tab Bar가 실제 View 선택 상태를 컴포넌트 내부에서 보장하도록 `view_text_tab_bar.xml` 루트를 `TextTabBarView`로 보완했다. 기존 화면 적용은 하지 않았다.
- 실행 명령/도구:
- `read(app/src/main/res/layout/view_title_bar_home.xml)`
- `read(app/src/main/res/layout/view_title_bar_default.xml)`
- `read(app/src/main/res/layout/view_text_tab_bar.xml)`
- `read(app/src/main/res/color/color_text_tab_bar.xml)`
- `read(app/src/main/java/kr/co/vividnext/sodalive/v2/widget/TextTabSelectionState.kt)`
- `read(app/src/main/java/kr/co/vividnext/sodalive/v2/widget/TextTabBarView.kt)`
- `read(app/src/test/java/kr/co/vividnext/sodalive/v2/widget/TextTabSelectionStateTest.kt)`
- `lsp_diagnostics` on modified XML/Kotlin files
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.TextTabSelectionStateTest"`
- `./gradlew :app:assembleDebug`
- `./gradlew :app:ktlintCheck`
- `rg --files app/build/generated/data_binding_base_class_source_out/debug/out | rg "View(TextTabBar|TitleBar)"`
- 결과:
- TDD RED 단계에서 `TextTabSelectionState` 미정의로 `TextTabSelectionStateTest`가 실패함을 먼저 확인했다.
- Kotlin/XML LSP 서버가 현재 환경에 설정되어 있지 않아 `lsp_diagnostics`는 실행 불가했다.
- `TextTabSelectionStateTest``BUILD SUCCESSFUL`로 완료됐다.
- `:app:assembleDebug``BUILD SUCCESSFUL`로 완료됐다.
- `:app:ktlintCheck``BUILD SUCCESSFUL`로 완료됐다.
- 2026-05-19
- 무엇/왜/어떻게: 탭바 컴포넌트 네이밍을 `CapsuleTabBarView` 방식에 맞춰 `TextTabBarView` 계열로 통일했다.
- 실행 명령/도구:
- `rg -n "TabText|tab_text|Tab Text|Tab-Text|view_tab_text_bar|color_tab_text_bar" app/src docs`
- `apply_patch(app/src/main/java/kr/co/vividnext/sodalive/v2/widget/TabTextBarView.kt -> TextTabBarView.kt)`
- `apply_patch(app/src/main/java/kr/co/vividnext/sodalive/v2/widget/TabTextSelectionState.kt -> TextTabSelectionState.kt)`
- `apply_patch(app/src/test/java/kr/co/vividnext/sodalive/v2/widget/TabTextSelectionStateTest.kt -> TextTabSelectionStateTest.kt)`
- `apply_patch(app/src/main/res/layout/view_tab_text_bar.xml -> view_text_tab_bar.xml)`
- `apply_patch(app/src/main/res/color/color_tab_text_bar.xml -> color_text_tab_bar.xml)`
- 결과:
- `TabTextBarView``TextTabBarView`로 변경했다.
- `TabTextSelectionState``TextTabSelectionState`로 변경했다.
- `TabTextSelectionStateTest``TextTabSelectionStateTest`로 변경했다.
- `view_tab_text_bar.xml``view_text_tab_bar.xml`로 변경했다.
- `color_tab_text_bar.xml``color_text_tab_bar.xml`로 변경했다.
- Text Tab 내부 view id도 `tv_text_tab_first`, `tv_text_tab_second`, `tv_text_tab_third`로 정리했다.
- 추가 검증:
- Kotlin/XML LSP 서버가 현재 환경에 설정되어 있지 않아 `lsp_diagnostics`는 실행 불가했다.
- 병렬 Gradle 실행 중 Kotlin compile 캐시 접근 오류가 한 번 발생해 순차 실행으로 재검증했다.
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.TextTabSelectionStateTest"``BUILD SUCCESSFUL`로 완료됐다.
- `./gradlew :app:assembleDebug``BUILD SUCCESSFUL`로 완료됐다.
- `./gradlew :app:ktlintCheck``BUILD SUCCESSFUL`로 완료됐다.
- `ViewTextTabBarBinding`, `ViewTitleBarDefaultBinding`, `ViewTitleBarHomeBinding` 생성 파일을 확인해 신규 컴포넌트 ViewBinding 생성 가능성을 확인했다.
- `git status --short` 기준 기존 화면 Activity/Fragment/Layout 파일은 변경하지 않았다.
- 2026-05-19
- 무엇/왜/어떻게: 코드 리뷰 피드백을 검토해 숨김 탭의 selected 상태가 남을 가능성을 제거하고, 이미 선택된 탭 재선택 시 중복 콜백을 발생시키지 않도록 보완했다.
- 실행 명령/도구:
- `oracle` read-only code review
- `apply_patch(app/src/main/java/kr/co/vividnext/sodalive/v2/widget/TextTabBarView.kt)`
- 결과:
- `TextTabBarView`는 메뉴가 줄어들어 숨겨지는 탭에도 `isSelected = false`를 적용한다.
- `TextTabBarView.selectTab()`은 이미 선택된 index를 다시 선택하면 상태와 콜백을 변경하지 않는다.
- `view_title_bar_home.xml``@drawable/img_text_logo_v2`를 참조하며, 현재 해당 이미지 파일은 git 기준 untracked 상태이므로 커밋 시 함께 포함해야 한다.
- 추가 검증:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.TextTabSelectionStateTest"``BUILD SUCCESSFUL`로 완료됐다.
- `./gradlew :app:assembleDebug``BUILD SUCCESSFUL`로 완료됐다.
- `./gradlew :app:ktlintCheck``BUILD SUCCESSFUL`로 완료됐다.
- 2026-05-19
- 무엇/왜/어떻게: 추가 요청에 따라 TitleBar 좌우 padding을 20dp로 지정하고, Tab-Text Bar 텍스트를 왼쪽 정렬 및 텍스트 사이 20dp 간격 구조로 변경했다.
- 실행 명령/도구:
- `read(app/src/main/res/layout/view_title_bar_home.xml)`
- `read(app/src/main/res/layout/view_title_bar_default.xml)`
- `read(app/src/main/res/layout/view_text_tab_bar.xml)`
- `apply_patch(app/src/main/res/layout/view_title_bar_home.xml)`
- `apply_patch(app/src/main/res/layout/view_title_bar_default.xml)`
- `apply_patch(app/src/main/res/layout/view_text_tab_bar.xml)`
- `lsp_diagnostics` on modified XML files
- `rg -n "paddingHorizontal=\"20dp\"|gravity=\"start\|center_vertical\"|layout_marginEnd=\"20dp\"|layout_width=\"wrap_content\"" app/src/main/res/layout/view_title_bar_home.xml app/src/main/res/layout/view_title_bar_default.xml app/src/main/res/layout/view_text_tab_bar.xml`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.TextTabSelectionStateTest"`
- `./gradlew :app:assembleDebug`
- `./gradlew :app:ktlintCheck`
- 결과:
- `view_title_bar_home.xml`, `view_title_bar_default.xml` 루트에 `android:paddingHorizontal="20dp"`를 적용했다.
- `view_text_tab_bar.xml` 루트 gravity를 `start|center_vertical`로 변경했다.
- Tab-Text Bar의 각 TextView는 `wrap_content` 폭을 사용하고, 첫 번째/두 번째 텍스트에 `android:layout_marginEnd="20dp"`를 적용해 텍스트 사이 간격을 20dp로 맞췄다.
- XML LSP 서버가 현재 환경에 설정되어 있지 않아 `lsp_diagnostics`는 실행 불가했다.
- `TextTabSelectionStateTest``BUILD SUCCESSFUL`로 완료됐다.
- `:app:assembleDebug``BUILD SUCCESSFUL`로 완료됐다.
- `:app:ktlintCheck``BUILD SUCCESSFUL`로 완료됐다.
- 2026-05-19
- 무엇/왜/어떻게: 추가 요청에 따라 이번에 추가한 컴포넌트 XML의 하드코딩 color/spacing/radius 값을 점검하고, 기존 `colors.xml`, `dimens.xml`에 정의된 값으로 변경 가능한 항목을 치환했다.
- 실행 명령/도구:
- `read(app/src/main/res/layout/view_title_bar_home.xml)`
- `read(app/src/main/res/layout/view_title_bar_default.xml)`
- `read(app/src/main/res/layout/view_text_tab_bar.xml)`
- `read(app/src/main/res/color/color_text_tab_bar.xml)`
- `read(app/src/main/res/values/colors.xml)`
- `read(app/src/main/res/values/dimens.xml)`
- `apply_patch(app/src/main/res/layout/view_title_bar_home.xml)`
- `apply_patch(app/src/main/res/layout/view_title_bar_default.xml)`
- `apply_patch(app/src/main/res/layout/view_text_tab_bar.xml)`
- `rg -n "\b[0-9]+dp\b|#[0-9A-Fa-f]{6,8}" app/src/main/res/layout/view_title_bar_home.xml app/src/main/res/layout/view_title_bar_default.xml app/src/main/res/layout/view_text_tab_bar.xml app/src/main/res/color/color_text_tab_bar.xml`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.TextTabSelectionStateTest"`
- `./gradlew :app:assembleDebug`
- `./gradlew :app:ktlintCheck`
- 결과:
- `20dp`로 하드코딩되어 있던 TitleBar horizontal padding, Tab-Text Bar horizontal padding, Tab-Text 간격을 `@dimen/spacing_20`으로 변경했다.
- Color 값은 이미 `@color/black`, `@color/white`, `@color/gray_600`, `@color/color_text_tab_bar`를 사용하고 있어 추가 치환할 하드코딩 hex 값은 없었다.
- Radius 값은 이번 컴포넌트에 없었다.
- 남아 있는 `60dp`, `52dp`는 컴포넌트 고정 높이 요구사항 값이며 현재 `dimens.xml`에 대응 토큰이 없어 변경하지 않았다.
- spacer용 `0dp``layout_weight` 사용을 위한 Android 레이아웃 관례 값이므로 리소스로 치환하지 않았다.
- `TextTabSelectionStateTest``BUILD SUCCESSFUL`로 완료됐다.
- `:app:assembleDebug``BUILD SUCCESSFUL`로 완료됐다.
- `:app:ktlintCheck``BUILD SUCCESSFUL`로 완료됐다.

View File

@@ -0,0 +1,105 @@
# PRD: 캡슐 탭바 컴포넌트
## 1. Overview
Figma `20:3590` 디자인을 기준으로 메뉴 개수가 많을 때 가로 스크롤되는 재사용 가능한 Capsule Tab Bar Component를 개발한다.
---
## 2. Problem
- 기존 `TextTabBarView`는 텍스트 탭 전용이며 최대 3개 메뉴를 전제로 한다.
- Figma `20:3590`은 capsule 형태 탭이 여러 개 나열되고, 메뉴가 화면 너비를 넘으면 가로 스크롤되어야 한다.
- 기존 Text Tab 요구사항과 capsule 탭의 높이, 패딩, radius, border, 스크롤 동작이 달라 같은 컴포넌트에 합치면 변경 범위와 회귀 위험이 커진다.
---
## 3. Goals
- 검정 배경, 높이 52dp의 재사용 가능한 Capsule Tab Bar를 제공한다.
- 탭 항목은 capsule 형태로 표시한다.
- 선택된 탭은 `soda_400` 배경과 white 텍스트로 표시한다.
- 선택되지 않은 탭은 black 배경, `gray_700` border, white 텍스트로 표시한다.
- 탭 내부 텍스트는 14sp medium 계열 Typography를 사용한다.
- 탭이 많아 화면 너비를 넘으면 가로로 스크롤되어야 한다.
- 메뉴 개수는 1개 이상을 허용하고, 항상 하나의 메뉴만 selected 상태여야 한다.
---
## 4. Non-Goals
- 기존 `TextTabBarView` 동작을 변경하지 않는다.
- 기존 화면에 신규 컴포넌트를 일괄 적용하지 않는다.
- Compose, 신규 Activity, Fragment, ViewModel을 추가하지 않는다.
- line 타입 또는 기존 text 타입 탭을 이번 컴포넌트에 통합하지 않는다.
- Figma에 없는 배지, 아이콘, 닫기 버튼, 스크롤 인디케이터를 추가하지 않는다.
---
## 5. Target Users
- XML 레이아웃을 작성하거나 유지보수하는 Android 개발자.
- 메뉴 개수가 동적으로 늘어날 수 있는 화면에서 capsule 탭 필터를 재사용하려는 개발자.
---
## 6. User Stories
- 개발자는 여러 개의 카테고리 메뉴를 capsule 형태로 표시하고 싶다.
- 개발자는 메뉴가 많을 때 화면 밖 메뉴를 가로 스크롤로 접근하게 하고 싶다.
- 개발자는 선택된 메뉴를 하나만 유지하고, 선택 변경 콜백으로 화면별 동작을 연결하고 싶다.
---
## 7. Core Features
### Capsule Tab Bar Component
Figma `20:3590` 기준 capsule 타입 탭바를 XML + Kotlin custom view로 제공한다.
#### Requirements
- 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-3590&m=dev
- Container width: `match_parent`
- Container height: `52dp`
- Container background: black
- Container horizontal start padding: `spacing_20`
- Tab list layout: horizontal, center vertical
- Tab gap: `spacing_8`
- Tab height: `34dp`
- Tab horizontal padding: `spacing_12`
- Tab vertical padding: `spacing_8`
- Tab corner radius: capsule 형태. 현재 `radius_100` 토큰이 없으므로 구현 시 신규 dimen 토큰 추가 또는 drawable radius 직접 지정 중 하나를 계획에서 명시한다.
- Selected tab: background `soda_400`, text white, no border
- Normal tab: background black, border `gray_700`, text white
- 메뉴가 화면 너비를 넘으면 가로 스크롤되어야 한다.
- 메뉴 개수는 1개 이상을 허용한다.
- selected index가 범위를 벗어나면 첫 번째 메뉴를 selected로 보정한다.
#### Edge Cases
- 메뉴가 0개이면 호출부 오류로 간주한다.
- 이미 선택된 탭을 다시 선택하면 중복 콜백을 호출하지 않는다.
- 메뉴 목록이 바뀌어 기존 selected index가 범위를 벗어나면 첫 번째 메뉴를 selected로 보정한다.
---
## 8. UX / UI Expectations
- 첫 번째 탭은 왼쪽 20dp 지점에서 시작한다.
- 각 탭은 8dp 간격으로 배치된다.
- 탭은 텍스트 길이에 따라 width가 늘어나며, 고정 균등 분배하지 않는다.
- 메뉴가 많으면 자연스럽게 좌우 스크롤된다.
- 스크롤 영역은 탭바 높이 52dp 안에서 세로 중앙 정렬되어야 한다.
---
## 9. Technical Constraints
- 현재 프로젝트는 XML Views + ViewBinding 기반이므로 XML 레이아웃과 Kotlin custom view 패턴을 사용한다.
- 신규 Kotlin 코드는 `kr.co.vividnext.sodalive.v2.widget` 패키지 하위에 둔다.
- 기존 `TextTabBarView``TextTabSelectionState`의 동작을 깨지 않는다.
- 기존 디자인 토큰 `soda_400`, `gray_700`, `white`, `black`, `spacing_8`, `spacing_12`, `spacing_20`을 우선 사용한다.
- 필요한 경우 capsule radius 전용 dimen 또는 drawable 리소스를 최소 범위로 추가한다.
---
## 10. Metrics
- 메뉴 1개 이상을 설정할 수 있고 항상 하나만 selected 상태다.
- 탭 항목이 화면 너비를 초과해도 가로 스크롤로 접근할 수 있다.
- Android 리소스 병합 및 디버그 빌드가 성공한다.
- 관련 선택 상태 테스트가 통과한다.
---
## 11. Open Questions
- 사용자 응답이 없으므로 기존 `TextTabBarView` 확장이 아닌 별도 `CapsuleTabBarView` 추가를 기본 설계로 확정한다.

View File

@@ -0,0 +1,134 @@
# PRD: 타이틀바 및 탭 텍스트바 컴포넌트
## 1. Overview
XML 레이아웃 기반 화면에서 재사용할 수 있는 Title Bar Component와 Tab-Text Bar Component를 개발한다.
---
## 2. Problem
- 화면마다 타이틀 영역과 탭 텍스트 영역을 개별 구현하면 높이, 배경색, 정렬, 텍스트 스타일이 달라질 수 있다.
- Home 화면과 일반 화면의 타이틀 바 구조는 유사하지만 좌측 영역의 콘텐츠가 다르므로 재사용 가능한 변형이 필요하다.
- 탭 텍스트 바는 최대 3개 메뉴와 단일 선택 상태를 일관되게 보장해야 한다.
---
## 3. Goals
- 전체 너비, 높이 60dp, 검정 배경, 세로 가운데 정렬을 갖는 재사용 가능한 Title Bar Component를 제공한다.
- Home Title Bar는 좌측에 `img_text_logo_v2`, 우측에 화면별로 교체 가능한 메뉴 아이콘을 배치할 수 있어야 한다.
- Default Title Bar는 좌측에 화면명 텍스트, 우측에 화면별로 교체 가능한 메뉴 아이콘을 배치할 수 있어야 한다.
- 전체 너비, 높이 52dp, 검정 배경, 세로 가운데 정렬을 갖는 재사용 가능한 Tab-Text Bar Component를 제공한다.
- Tab-Text Bar는 최대 3개 메뉴를 지원하고 반드시 하나만 selected 상태가 되도록 설계한다.
- Tab-Text Bar 텍스트는 `Typography.Heading3`을 사용하고 normal 색상은 `gray_600`, selected 색상은 white를 사용한다.
---
## 4. Non-Goals
- 이번 범위에서는 실제 화면에 컴포넌트를 일괄 적용하지 않는다.
- 신규 Activity, Fragment, ViewModel을 만들지 않는다.
- Compose 컴포넌트 또는 Compose Theme를 추가하지 않는다.
- 3개를 초과하는 탭 메뉴 지원, 스크롤 탭, 아이콘 탭, 배지 기능은 포함하지 않는다.
- 메뉴 아이콘 클릭 시 수행할 화면별 비즈니스 로직은 컴포넌트 내부에 고정하지 않는다.
- Figma에서 제공되지 않은 추가 애니메이션, 그림자, divider, pressed 효과는 추가하지 않는다.
---
## 5. Target Users
- XML 레이아웃을 작성하거나 유지보수하는 Android 개발자.
- 신규 v2 화면에서 공통 상단 바와 텍스트 탭 바를 재사용하려는 개발자.
---
## 6. User Stories
- 개발자는 Home 화면에서 로고와 메뉴 아이콘이 포함된 상단 바를 반복 구현 없이 재사용하고 싶다.
- 개발자는 일반 화면에서 화면명과 메뉴 아이콘이 포함된 상단 바를 같은 규격으로 사용하고 싶다.
- 개발자는 화면별 요구에 맞게 우측 메뉴 아이콘을 교체하고 클릭 동작을 주입하고 싶다.
- 개발자는 최대 3개 메뉴 중 하나만 선택되는 텍스트 탭 바를 재사용하고 싶다.
---
## 7. Core Features
### Title Bar Component
재사용 가능한 상단 타이틀 바를 XML 기반으로 제공한다.
#### Requirements
- Width: `match_parent`
- Height: `60dp`
- Alignment: 세로 가운데 정렬
- Background: black
- 공통 구조는 `좌측 콘텐츠 영역` + `빈 영역` + `우측 메뉴 아이콘 영역`으로 구성한다.
- 우측 메뉴 아이콘은 사용하는 화면마다 다른 drawable을 지정할 수 있어야 한다.
- 우측 메뉴 아이콘은 없는 화면에서도 사용할 수 있도록 숨김 처리가 가능해야 한다.
#### 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`를 표시한다.
- 중앙 빈 영역은 가변 폭으로 남은 공간을 채운다.
- 우측 메뉴 아이콘은 화면에서 교체 가능해야 한다.
#### 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
- 좌측 콘텐츠는 화면명 텍스트를 표시한다.
- 화면명 텍스트는 사용하는 화면마다 변경 가능해야 한다.
- 중앙 빈 영역은 가변 폭으로 남은 공간을 채운다.
- 우측 메뉴 아이콘은 화면에서 교체 가능해야 한다.
#### Edge Cases
- 메뉴 아이콘이 없으면 우측 아이콘 영역을 숨기거나 클릭 불가능한 상태로 둔다.
- Default Title Bar의 화면명이 비어 있으면 빈 텍스트 그대로 표시하지 않고 호출부에서 유효한 화면명을 제공해야 한다.
- `img_text_logo_v2` 리소스가 없는 경우 구현 단계에서 기존 로고 리소스 존재 여부를 먼저 확인하고, 없으면 해당 리소스를 추가 대상으로 명시한다.
### Tab-Text Bar Component
최대 3개 텍스트 메뉴를 보여주는 재사용 가능한 탭 바를 XML 기반으로 제공한다.
#### Requirements
- 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: `match_parent`
- Height: `52dp`
- Alignment: 세로 가운데 정렬
- Background: black
- Typography: `Typography.Heading3`
- Normal Color: `gray_600`
- Selected Color: white
- 메뉴 개수는 1개 이상 3개 이하만 허용한다.
- 메뉴 선택 시 선택된 메뉴만 selected 상태가 되고 나머지는 normal 상태가 되어야 한다.
- 초기 상태에서도 반드시 하나의 메뉴가 selected 상태여야 한다.
#### Edge Cases
- 메뉴가 0개이면 호출부 오류로 간주하고 컴포넌트 사용을 허용하지 않는다.
- 메뉴가 4개 이상이면 호출부 오류로 간주하고 컴포넌트 사용을 허용하지 않는다.
- selected index가 메뉴 범위를 벗어나면 첫 번째 메뉴를 selected로 보정하는 단순한 동작을 기본값으로 한다.
---
## 8. UX / UI Expectations
- Title Bar와 Tab-Text Bar는 모두 검정 배경 위에서 세로 중앙 정렬되어야 한다.
- Home Title Bar는 `img_text_logo_v2`와 우측 메뉴 아이콘 사이에 빈 영역이 자연스럽게 확장되어야 한다.
- Default Title Bar는 화면명과 우측 메뉴 아이콘 사이에 빈 영역이 자연스럽게 확장되어야 한다.
- Tab-Text Bar는 selected 텍스트가 white, normal 텍스트가 `gray_600`으로 명확히 구분되어야 한다.
- 터치 가능한 메뉴 아이콘과 탭 텍스트는 화면별 클릭 핸들러를 연결할 수 있어야 한다.
---
## 9. Technical Constraints
- 현재 프로젝트는 XML Views + ViewBinding 기반이므로 XML 레이아웃과 Kotlin View/ViewBinding 사용 패턴을 우선한다.
- 재사용 레이아웃은 `app/src/main/res/layout` 아래에 둔다.
- 기존 유사 패턴은 `toolbar_audio_content_main.xml`, `activity_audio_content_main.xml`, `activity_can_status.xml`, `fragment_message.xml`을 참고한다.
- 색상은 기존 `colors.xml`의 black, white, `gray_600` 토큰을 사용한다.
- Typography는 기존 `app/src/main/res/values/typography.xml``Typography.Heading3`을 사용한다.
- 기존 화면의 동작이나 레이아웃을 요청 없이 변경하지 않는다.
---
## 10. Metrics
- Title Bar 높이 60dp와 Tab-Text Bar 높이 52dp가 문서와 구현에서 일치한다.
- Home Title Bar와 Default Title Bar가 각각 요구된 좌측 콘텐츠를 지원한다.
- 메뉴 아이콘과 화면명은 호출 화면에서 변경 가능하다.
- Tab-Text Bar는 1~3개 메뉴만 허용하고 항상 하나의 selected 상태를 유지한다.
- Android 리소스 병합 및 디버그 빌드가 성공한다.
---
## 11. Open Questions
- Figma 조회는 현재 도구 타임아웃으로 확인하지 못했으므로, 구현 단계에서는 제공된 텍스트 요구사항을 우선 기준으로 삼는다.
- `img_text_logo_v2`가 이미 drawable 리소스로 존재하는지 구현 전 확인이 필요하다.