From c31e746ac616b8dabb4ffa2155edeadf02fa035c Mon Sep 17 00:00:00 2001 From: Sadturtleman Date: Tue, 25 Aug 2026 09:37:51 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(receiver):=20=EC=88=98=EC=8B=A0?= =?UTF-8?q?=EC=9E=90=20=ED=99=88=20=ED=97=A4=EB=8D=94=20=EC=95=A1=EC=85=98?= =?UTF-8?q?=EC=97=90=20=EC=9D=B4=EB=A6=84=EC=9D=84=20=EB=B6=99=EC=9D=B4?= =?UTF-8?q?=EA=B3=A0=20=EA=B5=AC=EC=84=B1=EC=9D=84=20=EA=B3=A0=EC=A0=95?= =?UTF-8?q?=ED=95=9C=EB=8B=A4=20(#613)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 상단바 아이콘 둘 다 contentDescription 이 null 이라, 유일하게 눌리는 설정 아이콘조차 스크린리더가 읽어 주지 못했다. 이름을 붙이고 clickable 에 Role.Button 을 준다. 수신자 홈에 프로필 아이콘이 없다는 것(시안 4327:73626)과 설정 아이콘이 눌린다는 것을 회귀 가드로 고정한다 — 회원 홈 상단바를 그대로 재사용하는 구조라 기본값(true)으로 되돌아가기 쉬운 자리다. 설정 진입의 목적지 정리는 이 PR 범위 밖이다 — 수신자 전용 설정 시안이 없어 docs/qa/assumptions.md 에 근거와 함께 남겼다. Co-Authored-By: Claude Opus 5 (1M context) --- .../afternote/core/ui/topbar/HomeTopBar.kt | 10 ++- core/ui/src/main/res/values/strings.xml | 3 + docs/qa/assumptions.md | 10 +++ .../home/ReceiverHomeHeaderTest.kt | 65 +++++++++++++++++++ 4 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/home/ReceiverHomeHeaderTest.kt diff --git a/core/ui/src/main/kotlin/com/afternote/core/ui/topbar/HomeTopBar.kt b/core/ui/src/main/kotlin/com/afternote/core/ui/topbar/HomeTopBar.kt index 0e6c4c5742..d803a68ff0 100644 --- a/core/ui/src/main/kotlin/com/afternote/core/ui/topbar/HomeTopBar.kt +++ b/core/ui/src/main/kotlin/com/afternote/core/ui/topbar/HomeTopBar.kt @@ -14,6 +14,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.afternote.core.ui.R @@ -46,7 +48,7 @@ fun HomeTopBar( if (showProfileIcon) { Image( painter = painterResource(R.drawable.core_ui_user), - contentDescription = null, + contentDescription = stringResource(R.string.core_ui_home_top_bar_profile), modifier = Modifier.size(18.dp), ) @@ -55,11 +57,13 @@ fun HomeTopBar( Image( painter = painterResource(R.drawable.core_ui_settings), - contentDescription = null, + // 유일하게 눌리는 액션인데 접근성 트리에 이름이 없었다 — 스크린리더가 + // "버튼" 으로만 읽는다 (#613). + contentDescription = stringResource(R.string.core_ui_home_top_bar_setting), modifier = Modifier .size(18.dp) - .clickable { onSettingClick() }, + .clickable(role = Role.Button) { onSettingClick() }, ) } }, diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index e30bac9ebe..2367aa2c5f 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -26,4 +26,7 @@ 네트워크 연결 오류 인터넷 연결을 확인한 후 다시 시도해 주세요. 다시 시도하기 + + 내 프로필 + 설정 \ No newline at end of file diff --git a/docs/qa/assumptions.md b/docs/qa/assumptions.md index c0e0367ea7..78bcb5b6c3 100644 --- a/docs/qa/assumptions.md +++ b/docs/qa/assumptions.md @@ -258,3 +258,13 @@ Compose 테스트로 고정했다. **분리한 것** — 일기의 기록 날짜는 생성·수정 어느 쪽으로도 서버에 보낼 수 없다(양쪽 스키마에 `date` 없음, 보내도 무시됨을 실측). 날짜 선택이 표시 전용인 문제는 BE 계약 확장이나 기획 결정이 필요해 #1008 로 분리했다. + +## #613 — 수신자 홈 설정 진입 (판단 보류) + +- 시안(수신자 섹션 `4327:73626`)의 수신자 홈 헤더에는 **톱니만** 있고 프로필 아이콘이 없다. + 코드도 이미 `showProfileIcon = false` 라 1번 지적은 해소된 상태였고, 재발 방지 가드만 붙였다. +- 톱니의 목적지는 확정하지 못했다. 같은 섹션의 「설정 메인」(`4327:75979`)은 회원 설정 + 화면(프로필 수정·비밀번호 변경·로그아웃)을 그대로 복제한 것이라 **수신자 전용 설정 + 시안은 없다**. 수신자는 X-Auth-Code 기반이라 로그아웃할 세션이 없다. +- 현 배선은 회원 설정 그래프(`Route.Setting`)로 그대로 보낸다. 무엇을 남길지(고객지원만, + 인증 해제, 진입점 제거)는 기획·디자인 확정이 필요해 이번 PR 범위에서 제외했다. diff --git a/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/home/ReceiverHomeHeaderTest.kt b/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/home/ReceiverHomeHeaderTest.kt new file mode 100644 index 0000000000..fa68944645 --- /dev/null +++ b/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/home/ReceiverHomeHeaderTest.kt @@ -0,0 +1,65 @@ +package com.afternote.feature.receiver.presentation.home + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import com.afternote.core.ui.R +import com.afternote.core.ui.theme.AfternoteTheme +import com.afternote.feature.receiver.presentation.home.model.ReceiverHomeUiState +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * 수신자 홈 헤더가 수신자가 실제로 할 수 있는 액션만 두는지 (#613, 시안 4327:73626). + * + * 종전에는 회원 홈과 같은 상단바를 그대로 써서 프로필 아이콘이 함께 떴는데, 목적지가 + * 없어 탭해도 아무 일도 없었다 — 접근성 트리에서도 clickable 이 아니라 사실상 장식이다. + * 시안의 수신자 홈에도 톱니 하나뿐이라 그 상태를 고정한다. + * + * 아이콘 이름도 함께 고정한다. 둘 다 `contentDescription = null` 이라 스크린리더가 + * 유일하게 눌리는 액션조차 읽어 주지 못했다. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [35]) +class ReceiverHomeHeaderTest { + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun `수신자 홈에는 프로필 아이콘이 없다`() { + composeRule.setContent { + AfternoteTheme { + ReceiverHomeScreen( + uiState = ReceiverHomeUiState.Loading, + onEvent = {}, + actions = ReceiverHomeActions(), + ) + } + } + + val profile = composeRule.activity.getString(R.string.core_ui_home_top_bar_profile) + composeRule.onNodeWithContentDescription(profile).assertDoesNotExist() + } + + @Test + fun `설정 아이콘은 이름이 있고 눌린다`() { + var settingClicks = 0 + composeRule.setContent { + AfternoteTheme { + ReceiverHomeScreen( + uiState = ReceiverHomeUiState.Loading, + onEvent = {}, + actions = ReceiverHomeActions(onSettingClick = { settingClicks += 1 }), + ) + } + } + + val setting = composeRule.activity.getString(R.string.core_ui_home_top_bar_setting) + composeRule.onNodeWithContentDescription(setting).assertIsDisplayed().assertHasClickAction() + } +} From 4a36605794e96e4b4e71b0996b52e7aef875fb64 Mon Sep 17 00:00:00 2001 From: Sadturtleman Date: Tue, 25 Aug 2026 16:00:06 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(core-ui):=20=EB=AA=A9=EC=A0=81=EC=A7=80?= =?UTF-8?q?=20=EC=97=86=EB=8A=94=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=EC=97=90=EB=8A=94=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=EC=9D=84=20=EB=91=90=EC=A7=80=20=EC=95=8A=EB=8A=94=EB=8B=A4=20?= =?UTF-8?q?(#613=20=EB=A6=AC=EB=B7=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이름을 붙이면 TalkBack 이 포커스 가능한 노드로 읽어, 사용자가 액션이 있다고 믿고 탭했다가 아무 반응도 얻지 못한다. 이 아이콘은 clickable 이 없는 장식이고, 실제로 그려지는 곳은 이 PR 범위 밖(회원 홈·애프터노트 홈·타임레터 홈)이다. 테스트가 존재 여부를 보려고 필요했던 앵커는 testTag 로 바꿔, 프로덕션 semantics 를 테스트 때문에 넓히지 않는다. 설정 아이콘의 이름·Role 은 그대로 둔다 — 그쪽은 실제로 눌리는 유일한 액션이다. Co-Authored-By: Claude Opus 5 (1M context) --- .../com/afternote/core/ui/topbar/HomeTopBar.kt | 11 +++++++++-- core/ui/src/main/res/values/strings.xml | 3 +-- .../presentation/home/ReceiverHomeHeaderTest.kt | 12 ++++++++---- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/core/ui/src/main/kotlin/com/afternote/core/ui/topbar/HomeTopBar.kt b/core/ui/src/main/kotlin/com/afternote/core/ui/topbar/HomeTopBar.kt index d803a68ff0..8b24a9d6aa 100644 --- a/core/ui/src/main/kotlin/com/afternote/core/ui/topbar/HomeTopBar.kt +++ b/core/ui/src/main/kotlin/com/afternote/core/ui/topbar/HomeTopBar.kt @@ -13,6 +13,7 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role @@ -20,6 +21,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.afternote.core.ui.R +/** 프로필 아이콘 앵커. 장식이라 semantics 이름이 없어 테스트가 이 태그로 존재 여부를 본다. */ +const val PROFILE_ICON_TEST_TAG = "home_top_bar_profile" + @OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeTopBar( @@ -46,10 +50,13 @@ fun HomeTopBar( .padding(end = 25.dp), ) { if (showProfileIcon) { + // 목적지가 없어 어디서도 눌리지 않는다 — 장식이므로 이름을 붙이지 않는다. + // 이름을 주면 TalkBack 이 포커스 가능한 노드로 읽어, 사용자가 액션이 있다고 + // 믿고 탭했다가 아무 반응도 얻지 못한다 (#613 리뷰). Image( painter = painterResource(R.drawable.core_ui_user), - contentDescription = stringResource(R.string.core_ui_home_top_bar_profile), - modifier = Modifier.size(18.dp), + contentDescription = null, + modifier = Modifier.size(18.dp).testTag(PROFILE_ICON_TEST_TAG), ) Spacer(modifier = Modifier.width(15.dp)) diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index 2367aa2c5f..caa01e4e4d 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -26,7 +26,6 @@ 네트워크 연결 오류 인터넷 연결을 확인한 후 다시 시도해 주세요. 다시 시도하기 - - 내 프로필 + 설정 \ No newline at end of file diff --git a/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/home/ReceiverHomeHeaderTest.kt b/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/home/ReceiverHomeHeaderTest.kt index fa68944645..e5a603be18 100644 --- a/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/home/ReceiverHomeHeaderTest.kt +++ b/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/home/ReceiverHomeHeaderTest.kt @@ -5,8 +5,10 @@ import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag import com.afternote.core.ui.R import com.afternote.core.ui.theme.AfternoteTheme +import com.afternote.core.ui.topbar.PROFILE_ICON_TEST_TAG import com.afternote.feature.receiver.presentation.home.model.ReceiverHomeUiState import org.junit.Rule import org.junit.Test @@ -21,8 +23,10 @@ import org.robolectric.annotation.Config * 없어 탭해도 아무 일도 없었다 — 접근성 트리에서도 clickable 이 아니라 사실상 장식이다. * 시안의 수신자 홈에도 톱니 하나뿐이라 그 상태를 고정한다. * - * 아이콘 이름도 함께 고정한다. 둘 다 `contentDescription = null` 이라 스크린리더가 - * 유일하게 눌리는 액션조차 읽어 주지 못했다. + * 설정 아이콘의 이름도 함께 고정한다. 종전에는 둘 다 `contentDescription = null` 이라 + * 스크린리더가 **유일하게 눌리는 액션**조차 읽어 주지 못했다. 프로필 아이콘은 반대로 + * 이름을 두지 않는다 — 목적지가 없어 어디서도 눌리지 않는 장식이고, 이름을 주면 TalkBack 이 + * 포커스 가능한 노드로 읽어 «눌러도 아무 일 없는 버튼» 이 된다 (리뷰 지적). */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [35]) @@ -42,8 +46,8 @@ class ReceiverHomeHeaderTest { } } - val profile = composeRule.activity.getString(R.string.core_ui_home_top_bar_profile) - composeRule.onNodeWithContentDescription(profile).assertDoesNotExist() + // 프로필 아이콘은 목적지가 없는 장식이라 semantics 이름이 없다 — 테스트 태그로 본다. + composeRule.onNodeWithTag(PROFILE_ICON_TEST_TAG).assertDoesNotExist() } @Test