diff --git a/feature/receiver/presentation/build.gradle.kts b/feature/receiver/presentation/build.gradle.kts
index b9a74a15d8..e45f27fa55 100644
--- a/feature/receiver/presentation/build.gradle.kts
+++ b/feature/receiver/presentation/build.gradle.kts
@@ -6,6 +6,8 @@ plugins {
}
android {
+ testOptions.unitTests.isIncludeAndroidResources = true
+
namespace = "com.afternote.feature.receiver.presentation"
experimentalProperties["android.experimental.enableScreenshotTest"] = true
}
@@ -36,6 +38,10 @@ dependencies {
testImplementation(libs.coroutines.test)
testImplementation(libs.robolectric)
+ // 실패 표기가 «레이아웃을 유지하는지» 는 실제로 그려 봐야 확인된다 (#952).
+ testImplementation(libs.androidx.compose.ui.test.junit4)
+ debugImplementation(libs.androidx.compose.ui.test.manifest)
+
screenshotTestImplementation(libs.screenshot.validation.api)
screenshotTestImplementation(libs.androidx.compose.ui.tooling)
}
diff --git a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/AfternoteSection.kt b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/AfternoteSection.kt
index 903620a00f..fb949c1d12 100644
--- a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/AfternoteSection.kt
+++ b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/AfternoteSection.kt
@@ -48,14 +48,10 @@ fun AfternoteSection(
title = stringResource(R.string.receiver_home_afternote_section_title),
description = stringResource(R.string.receiver_home_afternote_section_desc),
countLine =
- if (totalCount == null) {
- unavailableCountLine()
- } else {
- rememberCountLine(
- prefix = "${totalCount}개",
- suffix = "의 애프터노트가 있습니다.",
- )
- },
+ rememberCountLine(
+ prefix = "${countText(totalCount)}개",
+ suffix = "의 애프터노트가 있습니다.",
+ ),
buttonText = stringResource(R.string.receiver_home_afternote_section_button),
onButtonClick = onGoClick,
middleContent = {
diff --git a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/HomeSectionCard.kt b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/HomeSectionCard.kt
index 0988f205ef..423b3a46e3 100644
--- a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/HomeSectionCard.kt
+++ b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/HomeSectionCard.kt
@@ -88,6 +88,12 @@ fun rememberCountLine(
append(suffix)
}
-/** 조회 실패를 정상 0건과 구분하는 중립 문구. */
+/**
+ * 개수 자리에 넣을 문자열 — 값이 있으면 숫자, 없으면 대시 (#952).
+ *
+ * 갈래를 문장이 아니라 **개수 자리 하나로** 좁힌다. 문장을 통째로 갈아 끼우면 섹션 높이와
+ * 줄 구성이 바뀌고(시안 확정값은 레이아웃 유지), 호출부마다 같은 문장이 if/else 양쪽에
+ * 두 번 적혀 한쪽만 고쳐질 자리가 된다.
+ */
@Composable
-fun unavailableCountLine(): AnnotatedString = AnnotatedString(stringResource(R.string.receiver_home_section_count_unavailable))
+fun countText(count: Int?): String = count?.toString() ?: stringResource(R.string.receiver_home_section_count_unavailable)
diff --git a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/MindRecordSection.kt b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/MindRecordSection.kt
index 07dca23183..8a70956b2b 100644
--- a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/MindRecordSection.kt
+++ b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/MindRecordSection.kt
@@ -31,18 +31,15 @@ fun MindRecordSection(
title = stringResource(R.string.receiver_home_mindrecord_section_title),
description = stringResource(R.string.receiver_home_mindrecord_section_desc),
countLine =
- if (summary == null) {
- unavailableCountLine()
- } else {
- rememberCountLine(
- prefix = "${summary.totalCount}개 ",
- suffix = "마음의 기록이 있습니다.",
- )
- },
+ rememberCountLine(
+ prefix = "${countText(summary?.totalCount)}개 ",
+ suffix = "마음의 기록이 있습니다.",
+ ),
buttonText = stringResource(R.string.receiver_home_mindrecord_section_button),
onButtonClick = onGoClick,
middleContent = {
- if (summary == null) return@HomeSectionCard
+ // 조회 실패에도 카드를 숨기지 않는다 — 섹션 레이아웃 유지가 시안 확정값이고,
+ // 통째로 사라지면 «기능이 없는 것» 과 «못 불러온 것» 이 구분되지 않는다 (#952).
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
@@ -52,14 +49,14 @@ fun MindRecordSection(
iconResId = CoreUiR.drawable.core_ui_ic_mindrecord,
label = stringResource(R.string.receiver_home_mindrecord_daily_question),
totalLabel = totalLabel,
- count = summary.dailyQuestionCount,
+ count = summary?.dailyQuestionCount,
modifier = Modifier.weight(1f),
)
MindRecordStatCard(
iconResId = CoreUiR.drawable.core_ui_ic_diary,
label = stringResource(R.string.receiver_home_mindrecord_diary),
totalLabel = totalLabel,
- count = summary.diaryCount,
+ count = summary?.diaryCount,
modifier = Modifier.weight(1f),
)
}
diff --git a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/MindRecordStatCard.kt b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/MindRecordStatCard.kt
index dd9c4f131b..d1e811e7ca 100644
--- a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/MindRecordStatCard.kt
+++ b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/MindRecordStatCard.kt
@@ -20,11 +20,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.afternote.core.ui.theme.AfternoteDesign
import com.afternote.core.ui.theme.AfternoteTheme
+import com.afternote.feature.receiver.presentation.R
import com.afternote.core.ui.R as CoreUiR
/**
@@ -35,7 +37,8 @@ fun MindRecordStatCard(
iconResId: Int,
label: String,
totalLabel: String,
- count: Int,
+ /** 조회 실패·미상이면 null — 카드를 숨기지 않고 개수 자리만 대시로 둔다 (#952). */
+ count: Int?,
modifier: Modifier = Modifier,
) {
Column(
@@ -81,7 +84,7 @@ fun MindRecordStatCard(
)
Spacer(modifier = Modifier.height(2.dp))
Text(
- text = count.toString(),
+ text = count?.toString() ?: stringResource(R.string.receiver_home_section_count_unavailable),
style = AfternoteDesign.typography.h3,
color = AfternoteDesign.colors.gray9,
)
diff --git a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/TimeLetterSection.kt b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/TimeLetterSection.kt
index 98f94ad6bd..36d58b15d4 100644
--- a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/TimeLetterSection.kt
+++ b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/component/TimeLetterSection.kt
@@ -21,14 +21,10 @@ fun TimeLetterSection(
title = stringResource(R.string.receiver_home_timeletter_section_title),
description = stringResource(R.string.receiver_home_timeletter_section_desc),
countLine =
- if (totalCount == null) {
- unavailableCountLine()
- } else {
- rememberCountLine(
- prefix = "${totalCount}개 ",
- suffix = "라이프 이벤트 레터가 있습니다.",
- )
- },
+ rememberCountLine(
+ prefix = "${countText(totalCount)}개 ",
+ suffix = "라이프 이벤트 레터가 있습니다.",
+ ),
buttonText = stringResource(R.string.receiver_home_timeletter_section_button),
onButtonClick = onGoClick,
)
diff --git a/feature/receiver/presentation/src/main/res/values/strings.xml b/feature/receiver/presentation/src/main/res/values/strings.xml
index 5df3cbc3da..6ba371513f 100644
--- a/feature/receiver/presentation/src/main/res/values/strings.xml
+++ b/feature/receiver/presentation/src/main/res/values/strings.xml
@@ -14,7 +14,12 @@
애프터노트
고인이 사후 정리하고자 하는 데이터입니다.
애프터노트 확인하러 가기
- 기록 수를 불러오지 못했습니다.
+
+ –
모든 기록 내려받기
모든 기록을 내려받으시겠습니까?
모든 기록 내려받기에 실패했습니다.
diff --git a/feature/receiver/presentation/src/screenshotTestDebug/reference/com/afternote/feature/receiver/presentation/home/ReceiverHomeScreenScreenshotTestKt/receiverHomeScreenPartialFailureScreenshot_748aa731_0.png b/feature/receiver/presentation/src/screenshotTestDebug/reference/com/afternote/feature/receiver/presentation/home/ReceiverHomeScreenScreenshotTestKt/receiverHomeScreenPartialFailureScreenshot_748aa731_0.png
index d8dc314511..919e55998f 100644
Binary files a/feature/receiver/presentation/src/screenshotTestDebug/reference/com/afternote/feature/receiver/presentation/home/ReceiverHomeScreenScreenshotTestKt/receiverHomeScreenPartialFailureScreenshot_748aa731_0.png and b/feature/receiver/presentation/src/screenshotTestDebug/reference/com/afternote/feature/receiver/presentation/home/ReceiverHomeScreenScreenshotTestKt/receiverHomeScreenPartialFailureScreenshot_748aa731_0.png differ
diff --git a/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/home/component/ReceiverHomeCountFallbackTest.kt b/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/home/component/ReceiverHomeCountFallbackTest.kt
new file mode 100644
index 0000000000..963eac5b38
--- /dev/null
+++ b/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/home/component/ReceiverHomeCountFallbackTest.kt
@@ -0,0 +1,69 @@
+package com.afternote.feature.receiver.presentation.home.component
+
+import androidx.activity.ComponentActivity
+import androidx.compose.ui.test.assertCountEquals
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onAllNodesWithText
+import androidx.compose.ui.test.onNodeWithText
+import com.afternote.core.ui.theme.AfternoteTheme
+import com.afternote.feature.receiver.presentation.home.model.MindRecordSummary
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+/**
+ * 기록 수 조회 실패 표기 (#952, 시안 4309:19394).
+ *
+ * 확정값은 «개수 자리만 대시, 레이아웃은 유지» 다. 종전에는 문장을 통째로 "기록 수를
+ * 불러오지 못했습니다." 로 갈아 끼우고 통계 카드 행을 숨겨, 실패한 화면과 정상 화면의
+ * 구성이 달랐다 — 카드가 통째로 사라지면 «기능이 없는 것» 과 «못 불러온 것» 도
+ * 구분되지 않는다.
+ */
+@RunWith(RobolectricTestRunner::class)
+@Config(sdk = [35])
+class ReceiverHomeCountFallbackTest {
+ @get:Rule
+ val composeRule = createAndroidComposeRule()
+
+ @Test
+ fun `조회에 실패해도 문장 구조가 그대로다`() {
+ composeRule.setContent {
+ AfternoteTheme { MindRecordSection(summary = null, onGoClick = {}) }
+ }
+
+ // 종전 문구가 아니라, 개수 자리만 바뀐 같은 문장이어야 한다.
+ composeRule.onNodeWithText("–개 마음의 기록이 있습니다.").assertIsDisplayed()
+ }
+
+ @Test
+ fun `조회에 실패해도 통계 카드가 남는다`() {
+ composeRule.setContent {
+ AfternoteTheme { MindRecordSection(summary = null, onGoClick = {}) }
+ }
+
+ composeRule.onNodeWithText("데일리 질문").assertIsDisplayed()
+ composeRule.onNodeWithText("일기").assertIsDisplayed()
+ // 두 카드의 개수 자리가 «모두» 대시다 — 인덱스 하나만 보면 한 장만 대시로 남는
+ // 회귀를 놓치고, 노드 순서가 바뀌면 조용히 다른 것을 본다.
+ composeRule.onAllNodesWithText("–").assertCountEquals(2)
+ }
+
+ @Test
+ fun `정상 조회는 숫자를 그대로 보여준다`() {
+ composeRule.setContent {
+ AfternoteTheme {
+ MindRecordSection(
+ summary = MindRecordSummary(dailyQuestionCount = 18, diaryCount = 4),
+ onGoClick = {},
+ )
+ }
+ }
+
+ composeRule.onNodeWithText("22개 마음의 기록이 있습니다.").assertIsDisplayed()
+ composeRule.onNodeWithText("18").assertIsDisplayed()
+ composeRule.onNodeWithText("4").assertIsDisplayed()
+ }
+}