From 76e0958519e6d7d3c1fa6120cf4de8d17964d6ca Mon Sep 17 00:00:00 2001 From: Sadturtleman Date: Mon, 24 Aug 2026 12:23:50 +0900 Subject: [PATCH 1/3] =?UTF-8?q?refactor(mindrecord):=20core:ui=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B8=EC=9C=BC=EB=A1=9C=20=EC=A4=91=EB=B3=B5=206=EA=B1=B4?= =?UTF-8?q?=EC=9D=84=20=EC=88=98=EB=A0=B4=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feature/mindrecord/presentation 이 core:ui 에 이미 있는 컴포넌트·토큰·리소스를 쓰지 않고 같은 것을 모듈 안에 다시 만들어 쓰고 있었다. 6건 모두 core 가 먼저 존재한 뒤에 feature 구현이 들어왔고, 같은 모듈이 일부 화면에서는 이미 core 판을 쓰고 있어 모듈 안에서 두 방식이 공존했다. 1. 감정 버블 그레이 램프를 파일 상수 대신 토큰으로. top-level val 로는 @Composable 게터를 못 받아 BubbleSlot 이 색 대신 순위를 들게 하고 색 결정을 컴포저블 안으로 옮겼다. 배경이 토큰이 됐으므로 짝인 Color.White 3곳도 반전 토큰으로 옮긴다. 2. 섹션 헤더 재구현 2곳을 core AfternoteSectionHeader 로. 색이 토큰으로 수렴한다. 3. 삭제 액션 바 48줄을 core AfternoteButton(Variant5) 로. 자체 구현은 인용한 시안과 코너·라벨·divider·여백이 4/4 어긋나 있었고 core 는 4/4 일치한다. 선행으로 core DualActionLabel 에 Role.Button 을 더한다 — 자체본에는 있고 core 에는 없었다. 4. 레포에 마지막으로 남은 M3 OutlinedTextField 를 core AfternoteTextField 로. 5. 토큰 근사값 하드코딩 4곳을 토큰으로 (편차 2~7). 6. 리소스 중복 정리 — 아이콘 자체본 삭제하고 core 판으로, 닫기 문자열을 core 로, 참조 0건인 뒤로가기 문자열 삭제. Closes #634 --- .../core/ui/button/AfternoteButton.kt | 5 +- .../core/ui/button/AfternoteButtonTest.kt | 26 +++++++ .../presentation/component/DayCell.kt | 2 +- .../component/EmotionKeywordCard.kt | 58 +++++++++------ .../presentation/component/KeyBoardToolBar.kt | 3 +- .../presentation/component/LinkBottomSheet.kt | 45 ++++-------- .../model/MindRecordCategoryUi.kt | 6 +- .../screen/memoryspace/MemorySpaceScreen.kt | 2 +- .../receiver/ReceiverMindRecordScreen.kt | 2 +- .../presentation/screen/sender/DiaryScreen.kt | 15 +--- .../screen/sender/DraftListScreen.kt | 73 +++++-------------- .../presentation/screen/sender/HomeScreen.kt | 2 +- .../screen/sender/WeeklyReportScreen.kt | 20 +---- .../main/res/drawable/mindrecord_diary.xml | 15 ---- .../src/main/res/values/strings.xml | 2 - 15 files changed, 117 insertions(+), 159 deletions(-) delete mode 100644 feature/mindrecord/presentation/src/main/res/drawable/mindrecord_diary.xml diff --git a/core/ui/src/main/kotlin/com/afternote/core/ui/button/AfternoteButton.kt b/core/ui/src/main/kotlin/com/afternote/core/ui/button/AfternoteButton.kt index c8cfc5308c..93eddd211c 100644 --- a/core/ui/src/main/kotlin/com/afternote/core/ui/button/AfternoteButton.kt +++ b/core/ui/src/main/kotlin/com/afternote/core/ui/button/AfternoteButton.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription @@ -222,7 +223,9 @@ private fun RowScope.DualActionLabel( modifier = Modifier .weight(1f) - .clickable(onClick = onClick) + // 절반씩 독립 클릭이라 접근성 트리에도 버튼으로 잡혀야 한다. 없으면 + // 스크린리더가 이 영역을 눌 수 있는 요소로 읽지 않는다 (#634). + .clickable(role = Role.Button, onClick = onClick) .padding(vertical = AfternoteButtonVerticalPadding), contentAlignment = Alignment.Center, ) { diff --git a/core/ui/src/test/kotlin/com/afternote/core/ui/button/AfternoteButtonTest.kt b/core/ui/src/test/kotlin/com/afternote/core/ui/button/AfternoteButtonTest.kt index 6f5e9d019c..9dba69aed6 100644 --- a/core/ui/src/test/kotlin/com/afternote/core/ui/button/AfternoteButtonTest.kt +++ b/core/ui/src/test/kotlin/com/afternote/core/ui/button/AfternoteButtonTest.kt @@ -1,7 +1,9 @@ package com.afternote.core.ui.button +import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.assert import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithContentDescription @@ -122,4 +124,28 @@ class AfternoteButtonTest { assertTrue(secondaryClicked) } } + @Test + fun `dual-action 좌우 절반이 접근성 트리에 버튼으로 잡힌다`() { + // 절반씩 독립 클릭인데 Role.Button 이 없으면 스크린리더가 눌 수 있는 요소로 읽지 + // 않는다. mindrecord 자체 구현에는 있고 core 에는 없던 결함이다 (#634). + composeRule.setContent { + AfternoteTheme { + AfternoteButton( + text = "전체 삭제", + onClick = {}, + type = AfternoteButtonType.Variant5, + secondaryText = "선택 삭제", + onSecondaryClick = {}, + ) + } + } + + listOf("전체 삭제", "선택 삭제").forEach { label -> + composeRule + .onNodeWithText(label) + .assert( + SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button), + ) + } + } } diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/DayCell.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/DayCell.kt index b66658d839..fd1a9296b7 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/DayCell.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/DayCell.kt @@ -37,7 +37,7 @@ fun DayCell( // - NONE/UNANSWERED: 투명 배경 + 회색 글자, 인디케이터 없음 val bgColor = when (model.state) { - DayState.TODAY -> Color(0xFF1A1A1A) + DayState.TODAY -> AfternoteDesign.colors.gray9 else -> Color.Transparent } val textColor = diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/EmotionKeywordCard.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/EmotionKeywordCard.kt index 8304d781ac..a2f8fd69e9 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/EmotionKeywordCard.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/EmotionKeywordCard.kt @@ -51,7 +51,7 @@ fun EmotionKeywordCard( val capped = keywords.take(MAX_KEYWORDS) OutlinedCard( - colors = CardDefaults.cardColors(containerColor = Color.White), + colors = CardDefaults.cardColors(containerColor = AfternoteDesign.colors.white), border = BorderStroke(1.dp, AfternoteDesign.colors.gray2), modifier = modifier.fillMaxWidth(), shape = RoundedCornerShape(8.dp), @@ -103,7 +103,7 @@ private fun Bubble( .offset(x = slot.offsetX, y = slot.offsetY) .size(slot.size) .clip(CircleShape) - .background(slot.color), + .background(bubbleColor(slot.rank)), contentAlignment = Alignment.Center, ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { @@ -115,7 +115,8 @@ private fun Bubble( } else { AfternoteDesign.typography.bodySmallB }, - color = Color.White, + // 배경이 gray9~gray5 토큰이라 글자도 반전 토큰이라야 다크에서 뒤집힌다. + color = AfternoteDesign.colors.white, ) Text( text = keyword.count.toString(), @@ -135,70 +136,81 @@ private fun EmptyBubble() { .offset(x = slot.offsetX, y = slot.offsetY) .size(slot.size) .clip(CircleShape) - .background(slot.color), + .background(bubbleColor(slot.rank)), contentAlignment = Alignment.Center, ) { Text( text = "0", style = AfternoteDesign.typography.bodySmallR, - color = Color.White, + color = AfternoteDesign.colors.white, ) } } // ── Slot ────────────────────────────────────────────────────────────────────── +/** + * 버블 하나의 배치. 색은 **순위만** 들고 실제 값은 컴포저블 안에서 토큰으로 푼다. + * + * top-level `private val` 로는 `AfternoteDesign.colors`(@Composable 게터)를 받을 수 없어 + * 종전에는 그레이 램프를 파일 상수로 다시 적었다. 값이 토큰과 4/4 일치했고 같은 파일이 + * title·border 에는 이미 토큰을 쓰고 있어 의도적 예외도 아니었다 (#634). + */ private data class BubbleSlot( val size: Dp, val offsetX: Dp, val offsetY: Dp, - val color: Color, + val rank: Int, ) +/** 순위(1=가장 진함)를 그레이 토큰으로 푼다. 다크에서는 토큰이 알아서 반전된다. */ +@Composable +private fun bubbleColor(rank: Int): Color = + when (rank) { + 1 -> AfternoteDesign.colors.gray9 + 2 -> AfternoteDesign.colors.gray8 + 3 -> AfternoteDesign.colors.gray7 + else -> AfternoteDesign.colors.gray5 + } + private const val MAX_KEYWORDS = 4 private val BUBBLE_AREA_HEIGHT = 133.dp private val LARGE_TEXT_THRESHOLD = 72.dp -// 색상 순위: 1위(가장 진함) → 4위(가장 옅음) -private val ColorRank1 = Color(0xFF212121) -private val ColorRank2 = Color(0xFF424242) -private val ColorRank3 = Color(0xFF616161) -private val ColorRank4 = Color(0xFF9E9E9E) - // 0건 안내용 빈 검은 원. private val EMPTY_SLOT = - BubbleSlot(size = 96.dp, offsetX = 112.dp, offsetY = 15.dp, color = ColorRank1) + BubbleSlot(size = 96.dp, offsetX = 112.dp, offsetY = 15.dp, rank = 1) private fun slotsFor(count: Int): List = when (count) { 1 -> { listOf( - BubbleSlot(96.dp, 112.dp, 15.dp, ColorRank1), + BubbleSlot(96.dp, 112.dp, 15.dp, 1), ) } 2 -> { listOf( - BubbleSlot(96.dp, 78.dp, 0.dp, ColorRank1), - BubbleSlot(72.dp, 171.dp, 47.dp, ColorRank2), + BubbleSlot(96.dp, 78.dp, 0.dp, 1), + BubbleSlot(72.dp, 171.dp, 47.dp, 2), ) } 3 -> { listOf( - BubbleSlot(96.dp, 48.dp, 4.dp, ColorRank1), - BubbleSlot(72.dp, 141.dp, 51.dp, ColorRank2), - BubbleSlot(56.dp, 205.dp, 25.dp, ColorRank3), + BubbleSlot(96.dp, 48.dp, 4.dp, 1), + BubbleSlot(72.dp, 141.dp, 51.dp, 2), + BubbleSlot(56.dp, 205.dp, 25.dp, 3), ) } else -> { // 4 이상은 4 로 cap listOf( - BubbleSlot(96.dp, 37.dp, 30.dp, ColorRank1), - BubbleSlot(72.dp, 124.dp, 0.dp, ColorRank2), - BubbleSlot(56.dp, 149.dp, 72.dp, ColorRank3), - BubbleSlot(64.dp, 200.dp, 36.dp, ColorRank4), + BubbleSlot(96.dp, 37.dp, 30.dp, 1), + BubbleSlot(72.dp, 124.dp, 0.dp, 2), + BubbleSlot(56.dp, 149.dp, 72.dp, 3), + BubbleSlot(64.dp, 200.dp, 36.dp, 4), ) } } diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/KeyBoardToolBar.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/KeyBoardToolBar.kt index 69713f2e23..53c5e9eeb9 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/KeyBoardToolBar.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/KeyBoardToolBar.kt @@ -35,6 +35,7 @@ import com.afternote.core.ui.theme.AfternoteDesign import com.afternote.feature.mindrecord.presentation.R import com.afternote.feature.mindrecord.presentation.model.TextStyleState import com.afternote.feature.mindrecord.presentation.model.TextStyleType +import com.afternote.core.ui.R as CoreUiR @Composable fun BottomToolbar( @@ -218,7 +219,7 @@ private fun CloseButton(onClick: () -> Unit) { ) { Icon( painter = painterResource(R.drawable.mindrecord_close), - contentDescription = stringResource(R.string.mindrecord_toolbar_close_cd), + contentDescription = stringResource(CoreUiR.string.core_ui_content_description_close), tint = AfternoteDesign.colors.gray9, modifier = Modifier.size(10.dp), ) diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/LinkBottomSheet.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/LinkBottomSheet.kt index e680b93569..a30d79e154 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/LinkBottomSheet.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/LinkBottomSheet.kt @@ -14,11 +14,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text -import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -29,8 +29,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.afternote.core.ui.AfternoteTextField import com.afternote.core.ui.theme.AfternoteDesign import com.afternote.core.ui.theme.AfternoteTheme import com.afternote.feature.mindrecord.presentation.R @@ -48,7 +50,8 @@ fun LinkBottomSheet( onConfirm: (String) -> Unit, modifier: Modifier = Modifier, ) { - var url by remember { mutableStateOf("") } + // core 정본이 `TextFieldState` 를 받는다. 상태 소유자가 이 로컬 하나뿐이라 국소 변경이다 (#634). + val urlState = rememberTextFieldState() ModalBottomSheet( onDismissRequest = onDismiss, @@ -66,9 +69,8 @@ fun LinkBottomSheet( }, ) { LinkSheetContent( - url = url, - onUrlChange = { url = it }, - onConfirm = { onConfirm(url) }, + urlState = urlState, + onConfirm = { onConfirm(urlState.text.toString()) }, modifier = modifier, ) } @@ -76,8 +78,7 @@ fun LinkBottomSheet( @Composable private fun LinkSheetContent( - url: String, - onUrlChange: (String) -> Unit, + urlState: TextFieldState, onConfirm: () -> Unit, modifier: Modifier = Modifier, ) { @@ -109,25 +110,12 @@ private fun LinkSheetContent( ConfirmPill(onClick = onConfirm) } - OutlinedTextField( - value = url, - onValueChange = onUrlChange, - singleLine = true, - placeholder = { - Text( - text = stringResource(R.string.mindrecord_link_sheet_placeholder), - style = AfternoteDesign.typography.bodyBase, - color = AfternoteDesign.colors.gray4, - ) - }, - shape = RoundedCornerShape(8.dp), - colors = - TextFieldDefaults.colors( - focusedContainerColor = Color.White, - unfocusedContainerColor = Color.White, - focusedIndicatorColor = AfternoteDesign.colors.gray2, - unfocusedIndicatorColor = AfternoteDesign.colors.gray2, - ), + // core 정본을 쓴다. 종전에는 흰 배경 + gray2 indicator + 8dp 코너를 M3 프리미티브로 + // 다시 적었고, 레포 전체에서 M3 OutlinedTextField 를 쓰는 마지막 자리였다 (#634). + AfternoteTextField( + state = urlState, + placeholder = stringResource(R.string.mindrecord_link_sheet_placeholder), + keyboardType = KeyboardType.Uri, modifier = Modifier.fillMaxWidth(), ) } @@ -157,8 +145,7 @@ private fun ConfirmPill(onClick: () -> Unit) { private fun LinkBottomSheetPreview() { AfternoteTheme { LinkSheetContent( - url = "", - onUrlChange = {}, + urlState = rememberTextFieldState(), onConfirm = {}, ) } diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/model/MindRecordCategoryUi.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/model/MindRecordCategoryUi.kt index c03821c737..b23dc31929 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/model/MindRecordCategoryUi.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/model/MindRecordCategoryUi.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.afternote.core.model.MindRecordCategory import com.afternote.feature.mindrecord.presentation.R +import com.afternote.core.ui.R as CoreUiR sealed class MindRecordCategoryUi( val category: MindRecordCategory, @@ -40,7 +41,10 @@ sealed class MindRecordCategoryUi( category = MindRecordCategory.DIARY, titleRes = R.string.mindrecord_category_diary_title, descriptionRes = R.string.mindrecord_category_diary_description, - imageRes = R.drawable.mindrecord_diary, + // core 정본을 쓴다. 자체본은 pathData 가 문자 단위로 같고 strokeColor 와 반투명 + // 처리만 달랐는데, 같은 모듈의 나머지 3곳이 이미 core 판을 그대로 쓰고 있어 + // 한 아이콘이 두 출처로 갈려 있었다 (#634). + imageRes = CoreUiR.drawable.core_ui_ic_diary, ) { @Composable override fun DayIndicator( diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/memoryspace/MemorySpaceScreen.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/memoryspace/MemorySpaceScreen.kt index ad4e8b4e92..4000e0e9ae 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/memoryspace/MemorySpaceScreen.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/memoryspace/MemorySpaceScreen.kt @@ -86,7 +86,7 @@ private fun MemorySpaceContent( modifier = modifier .fillMaxSize() - .background(Color(0xFFF5F5F5)), + .background(AfternoteDesign.colors.gray1), ) { Box( modifier = diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/receiver/ReceiverMindRecordScreen.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/receiver/ReceiverMindRecordScreen.kt index 0c9f30c1d1..bd3e50f351 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/receiver/ReceiverMindRecordScreen.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/receiver/ReceiverMindRecordScreen.kt @@ -120,7 +120,7 @@ private fun SuccessContent( TabRowDefaults.PrimaryIndicator( modifier = Modifier.tabIndicatorOffset(selectedIndex, matchContentSize = false), width = 80.dp, - color = Color(0xFF1F1F1F), + color = AfternoteDesign.colors.gray9, ) }, ) { diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/DiaryScreen.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/DiaryScreen.kt index b25ba97100..73740d7516 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/DiaryScreen.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/DiaryScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.afternote.core.ui.AfternoteSectionHeader import com.afternote.core.ui.asString import com.afternote.core.ui.theme.AfternoteDesign import com.afternote.core.ui.theme.AfternoteTheme @@ -123,17 +124,9 @@ private fun DiaryListContent( } item { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = "DAILY ANSWER", - style = AfternoteDesign.typography.mono, - color = AfternoteDesign.colors.black.copy(alpha = 0.4f), - ) - HorizontalDivider(modifier = Modifier.padding(start = 12.dp)) - } + // core 정본을 쓴다. 종전에는 같은 구조를 인라인으로 다시 적어, 색이 + // black.copy(0.4f)·M3 baseline divider 로 토큰에서 벗어나 있었다 (#634). + AfternoteSectionHeader(title = "DAILY ANSWER") Spacer(modifier = Modifier.height(10.dp)) } diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/DraftListScreen.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/DraftListScreen.kt index 788f3ad7fa..0152aac08f 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/DraftListScreen.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/DraftListScreen.kt @@ -37,6 +37,8 @@ import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.afternote.core.ui.asString +import com.afternote.core.ui.button.AfternoteButton +import com.afternote.core.ui.button.AfternoteButtonType import com.afternote.core.ui.button.AfternoteCircularCheckbox import com.afternote.core.ui.button.CheckboxState import com.afternote.core.ui.theme.AfternoteDesign @@ -212,11 +214,16 @@ private fun SuccessContent( } } - // Figma 2372:24660 — 선택 모드 하단 "전체 삭제 | 선택 삭제" 바 + // Figma 2372:24660 — 선택 모드 하단 "전체 삭제 | 선택 삭제" 바. + // core 정본을 쓴다. 자체 구현은 같은 시안을 근거로 적었는데 코너·라벨·divider·여백이 + // 4/4 어긋나 있었고, core 는 4/4 일치한다 (#634). if (selectionMode) { - DeleteActionBar( - onDeleteAll = onDeleteAll, - onDeleteSelected = onDeleteSelected, + AfternoteButton( + text = stringResource(MindRecordR.string.mindrecord_draft_list_delete_all), + onClick = onDeleteAll, + type = AfternoteButtonType.Variant5, + secondaryText = stringResource(MindRecordR.string.mindrecord_draft_list_delete_selected), + onSecondaryClick = onDeleteSelected, modifier = Modifier .align(Alignment.BottomCenter) @@ -286,56 +293,6 @@ private fun DraftRow( } } -@Composable -private fun DeleteActionBar( - onDeleteAll: () -> Unit, - onDeleteSelected: () -> Unit, - modifier: Modifier = Modifier, -) { - Row( - modifier = - modifier - .fillMaxWidth() - .clip(RoundedCornerShape(10.dp)) - .background(AfternoteDesign.colors.gray9) - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Box( - modifier = - Modifier - .weight(1f) - .clickable(role = Role.Button, onClick = onDeleteAll) - .padding(vertical = 12.dp), - contentAlignment = Alignment.Center, - ) { - Text( - text = stringResource(MindRecordR.string.mindrecord_draft_list_delete_all), - style = AfternoteDesign.typography.bodySmallB, - color = AfternoteDesign.colors.white, - ) - } - VerticalDivider( - modifier = Modifier.height(16.dp), - color = AfternoteDesign.colors.gray6, - ) - Box( - modifier = - Modifier - .weight(1f) - .clickable(role = Role.Button, onClick = onDeleteSelected) - .padding(vertical = 12.dp), - contentAlignment = Alignment.Center, - ) { - Text( - text = stringResource(MindRecordR.string.mindrecord_draft_list_delete_selected), - style = AfternoteDesign.typography.bodySmallB, - color = AfternoteDesign.colors.white, - ) - } - } -} - @Composable private fun DeletedToast(modifier: Modifier = Modifier) { Box( @@ -383,7 +340,13 @@ private fun DraftRowPreview() { selected = true, onToggleSelect = {}, ) - DeleteActionBar(onDeleteAll = {}, onDeleteSelected = {}) + AfternoteButton( + text = "전체 삭제", + onClick = {}, + type = AfternoteButtonType.Variant5, + secondaryText = "선택 삭제", + onSecondaryClick = {}, + ) } } } diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/HomeScreen.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/HomeScreen.kt index ce76864003..2837a98da5 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/HomeScreen.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/HomeScreen.kt @@ -150,7 +150,7 @@ fun HomeScreen( matchContentSize = false, ), width = 80.dp, - color = Color(0xFF1F1F1F), + color = AfternoteDesign.colors.gray9, ) }, ) { diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/WeeklyReportScreen.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/WeeklyReportScreen.kt index 3f2a4202d2..6d5a7e61bd 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/WeeklyReportScreen.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/WeeklyReportScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.afternote.core.ui.AfternoteSectionHeader import com.afternote.core.ui.asString import com.afternote.core.ui.theme.AfternoteDesign import com.afternote.core.ui.theme.AfternoteTheme @@ -100,7 +101,7 @@ private fun WeeklyReportContent( modifier = Modifier.padding(vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { - SectionHeader(title = "TOP KEYWORDS") + AfternoteSectionHeader(title = "TOP KEYWORDS") EmotionKeywordCard( keywords = state.emotionKeywords, descriptionText = @@ -116,7 +117,7 @@ private fun WeeklyReportContent( // HISTORY 섹션 — gap=12 (divider + 다이어리 카드들). item { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - SectionHeader(title = "HISTORY") + AfternoteSectionHeader(title = "HISTORY") state.dailyQuestions.forEach { dailyQuestion -> DailyQuestionListCard(answer = dailyQuestion) } @@ -159,21 +160,6 @@ private fun recordedSummary( } } -@Composable -private fun SectionHeader(title: String) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = title, - style = AfternoteDesign.typography.mono, - color = AfternoteDesign.colors.black.copy(alpha = 0.4f), - ) - HorizontalDivider(modifier = Modifier.padding(start = 12.dp)) - } -} - @Composable private fun LoadingBox(modifier: Modifier = Modifier) { Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { diff --git a/feature/mindrecord/presentation/src/main/res/drawable/mindrecord_diary.xml b/feature/mindrecord/presentation/src/main/res/drawable/mindrecord_diary.xml deleted file mode 100644 index 26320b114b..0000000000 --- a/feature/mindrecord/presentation/src/main/res/drawable/mindrecord_diary.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/feature/mindrecord/presentation/src/main/res/values/strings.xml b/feature/mindrecord/presentation/src/main/res/values/strings.xml index ea7f4f8d8e..d9e9c77ec9 100644 --- a/feature/mindrecord/presentation/src/main/res/values/strings.xml +++ b/feature/mindrecord/presentation/src/main/res/values/strings.xml @@ -138,7 +138,6 @@ 왼쪽 정렬 가운데 정렬 오른쪽 정렬 - 닫기 임시저장 %d 임시저장 텍스트 설정 @@ -152,7 +151,6 @@ %d 글자 - 뒤로가기 더보기 From 84f945a1ed00bfc40457ecd26e11ea63b3225705 Mon Sep 17 00:00:00 2001 From: Sadturtleman Date: Mon, 24 Aug 2026 14:58:22 +0900 Subject: [PATCH 2/3] =?UTF-8?q?style(core-ui):=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=84=A0=EC=96=B8=20=EC=95=9E=20=EB=B9=88=20?= =?UTF-8?q?=EC=A4=84=EC=9D=84=20=EB=84=A3=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ktlint blank-line-before-declaration 위반. 리뷰 지적 (#634). Co-Authored-By: Claude Opus 5 (1M context) --- .../kotlin/com/afternote/core/ui/button/AfternoteButtonTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/ui/src/test/kotlin/com/afternote/core/ui/button/AfternoteButtonTest.kt b/core/ui/src/test/kotlin/com/afternote/core/ui/button/AfternoteButtonTest.kt index 9dba69aed6..2f2970bee5 100644 --- a/core/ui/src/test/kotlin/com/afternote/core/ui/button/AfternoteButtonTest.kt +++ b/core/ui/src/test/kotlin/com/afternote/core/ui/button/AfternoteButtonTest.kt @@ -124,6 +124,7 @@ class AfternoteButtonTest { assertTrue(secondaryClicked) } } + @Test fun `dual-action 좌우 절반이 접근성 트리에 버튼으로 잡힌다`() { // 절반씩 독립 클릭인데 Role.Button 이 없으면 스크린리더가 눌 수 있는 요소로 읽지 From b9acb5bf34238c679e360b93a76d41efd12b2138 Mon Sep 17 00:00:00 2001 From: Sadturtleman Date: Mon, 24 Aug 2026 22:50:13 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat(core-ui):=20dual-action=20=EB=B3=B4?= =?UTF-8?q?=EC=A1=B0=20=EB=9D=BC=EB=B2=A8=20=EB=B9=84=ED=99=9C=EC=84=B1=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EB=A5=BC=20=EC=A0=95=EB=B3=B8=EC=97=90=20?= =?UTF-8?q?=EB=84=A3=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 자체 구현에는 «선택 0개면 선택 삭제를 막는다» 가 있는데(#442) 정본에는 그 상태를 표현할 파라미터가 없었다. 정본이 실사용 요구를 못 담으면 수렴이 성립하지 않는다. 비활성은 클릭 차단과 색(gray6)으로 함께 드러낸다 — 눌리지 않는데 눌릴 것처럼 보이면 고장으로 읽힌다. 리뷰 지적. Co-Authored-By: Claude Opus 5 (1M context) --- .../afternote/core/ui/button/AfternoteButton.kt | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/core/ui/src/main/kotlin/com/afternote/core/ui/button/AfternoteButton.kt b/core/ui/src/main/kotlin/com/afternote/core/ui/button/AfternoteButton.kt index 93eddd211c..9cf8ce0609 100644 --- a/core/ui/src/main/kotlin/com/afternote/core/ui/button/AfternoteButton.kt +++ b/core/ui/src/main/kotlin/com/afternote/core/ui/button/AfternoteButton.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalMinimumInteractiveComponentSize import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -58,6 +59,8 @@ private val AfternoteButtonVerticalPadding = 13.dp * (예: 전체 삭제 | 선택 삭제), 생략하면 버튼 전체가 [onClick] 하나로 눌리는 기존 동작 그대로다. * * @param onSecondaryClick Variant5 dual-action 모드에서 오른쪽 절반([secondaryText]) 클릭 콜백 + * @param isSecondaryEnabled dual-action 의 **보조 라벨만** 비활성. 선택이 0개일 때 «선택 삭제» 를 + * 막는 자리다 (#442) — 자체 구현에는 있던 상태라 정본이 담지 않으면 수렴이 성립하지 않는다. * @param isLoading true 면 라벨 대신 스피너를 그리고 클릭을 막는다 (네트워크 대기 등 진행 중 표시). * dual-action 모드도 로딩 중엔 단일 스피너 바로 렌더되어 양쪽 클릭이 모두 막힌다. * 접근성 이름은 [text] 로 유지되고, 로딩 상태는 stateDescription 으로 노출된다. @@ -72,6 +75,7 @@ fun AfternoteButton( secondaryText: String? = null, containerColor: Color? = null, onSecondaryClick: (() -> Unit)? = null, + isSecondaryEnabled: Boolean = true, ) { // 색 테이블을 여기서 한 번만 결정해 두 렌더 경로(단일 Surface·dual-action)에 같은 값을 공급한다 — // dual 쪽에 Variant5 색을 재하드코딩하면 테이블 변경 시 조용히 어긋난다. @@ -101,6 +105,7 @@ fun AfternoteButton( onClick = onClick, secondaryText = secondaryText, onSecondaryClick = onSecondaryClick, + isSecondaryEnabled = isSecondaryEnabled, containerColor = resolvedContainerColor, contentColor = contentColor, modifier = modifier, @@ -195,6 +200,7 @@ private fun DualActionButtonSurface( onClick: () -> Unit, secondaryText: String, onSecondaryClick: () -> Unit, + isSecondaryEnabled: Boolean, containerColor: Color, contentColor: Color, modifier: Modifier = Modifier, @@ -208,7 +214,11 @@ private fun DualActionButtonSurface( Row(verticalAlignment = Alignment.CenterVertically) { DualActionLabel(text = text, onClick = onClick) Variant5LabelDivider() - DualActionLabel(text = secondaryText, onClick = onSecondaryClick) + DualActionLabel( + text = secondaryText, + onClick = onSecondaryClick, + isEnabled = isSecondaryEnabled, + ) } } } @@ -218,6 +228,7 @@ private fun DualActionButtonSurface( private fun RowScope.DualActionLabel( text: String, onClick: () -> Unit, + isEnabled: Boolean = true, ) { Box( modifier = @@ -225,13 +236,15 @@ private fun RowScope.DualActionLabel( .weight(1f) // 절반씩 독립 클릭이라 접근성 트리에도 버튼으로 잡혀야 한다. 없으면 // 스크린리더가 이 영역을 눌 수 있는 요소로 읽지 않는다 (#634). - .clickable(role = Role.Button, onClick = onClick) + .clickable(role = Role.Button, enabled = isEnabled, onClick = onClick) .padding(vertical = AfternoteButtonVerticalPadding), contentAlignment = Alignment.Center, ) { Text( text = text, style = AfternoteDesign.typography.captionLargeB, + // 비활성은 색으로도 드러나야 한다 — 눌리지 않는데 눌릴 것처럼 보이면 고장으로 읽힌다. + color = if (isEnabled) LocalContentColor.current else AfternoteDesign.colors.gray6, ) } }