Skip to content

fix(home): 조회 전 수신인 지정 배지를 '미완료' 로 확정하지 않는다 (#698) - #1021

Merged
Sadturtleman merged 4 commits into
developfrom
fix/698-recipient-badge-loading
Aug 24, 2026
Merged

Sadturtleman merged 4 commits into
developfrom
fix/698-recipient-badge-loading

Conversation

@Sadturtleman

Copy link
Copy Markdown
Collaborator

Closes #698

무엇이 문제였나

홈 로딩 분기가 isRecipientDesignated = false 를 넘겨 아직 조회되지 않은 상태를 「미완료」라는 결과로 단정했다. 이미 수신인을 지정한 사용자도 진입 동안 그 배지를 보고, 배지가 탭 가능한 CTA(Incomplete) 라 하지 않아도 될 행동을 유도했다.

같은 분기에서 오늘의 질문은 시머 스켈레톤, 카테고리 카운트는 categoryCountsLoading = true 로 미결정을 미결정으로 표현한다. 배지만 결과를 확정해 표현이 어긋나 있었다.

무엇을 했나

이슈의 두 가지 요구를 그대로 따랐다.

  • RecipientDesignationBadgeState.Unknown 신설 — 문구 없이 자리만 잡고 shimmer 로 «아직 오는 중» 을 알린다. 클릭 대상이 아니다
  • HomeTabScrollContent(isRecipientDesignated: Boolean?) — 로딩 분기는 null 을 넘긴다. false 를 확정값처럼 흘리지 않는다

TalkBack 이 읽을 것이 없으면 화면에 무슨 일이 있는지 알 수 없으므로 자리 표시 문구(수신인 지정 여부 확인 중)를 두되, 확정 문구("완료"·"미완료")는 쓰지 않는다.

회귀 가드

RecipientBadgeLoadingTest 4개 — Robolectric + Compose. shimmer 가 무한 애니메이션이라 Compose 가 영원히 idle 이 되지 않으므로 mainClock.autoAdvance = false 로 한 프레임만 그려 읽는다.

컴포넌트만이 아니라 화면 배선까지 본다. 컴포넌트에 Unknown 을 두고도 화면이 여전히 false 를 넘기면 증상이 그대로 남기 때문이다.

로딩 분기를 false 로 되돌림 → 4 tests completed, 1 failed
  홈 로딩 분기가 미완료 배지를 그리지 않는다
원복 → 4개 통과

⚠️ 스크린샷 baseline 갱신 필요 (머지 전)

homeTabScreenLoadingScreenshot 이 HomeTabUiState.Loading 을 그리므로 의도된 시각 변경으로 baseline 이 어긋난다. CI 의 Validate Compose Preview Screenshots 가 실패한다.

제 환경에 Docker 가 올라와 있지 않아 갱신하지 못했다. README 가 «host 직접 실행은 CI 와 baseline 이 어긋난다» 고 명시하고 있어 macOS 에서 만들지 않았다.

docker run --rm -v "$PWD":/workspace -w /workspace afternote-screenshot:latest \
  ./gradlew :feature:home:presentation:updateScreenshotTest

diff 는 «로딩 화면의 미완료 배지가 shimmer 자리 표시로 바뀐» 한 장이어야 한다. core:ui 의 배지 baseline 2종(Completed·Incomplete)은 렌더가 그대로라 영향이 없다 — Unknown 용 preview 는 baseline 을 만들 수 없어 추가하지 않았다.

실기기 관찰

에뮬레이터에서 진입 직후를 8회 연속 덤프했는데 로딩 창이 덤프 간격(~0.6s)보다 짧아 고치기 전 빌드에서도 「미완료」가 잡히지 않았다. 즉 이 결함은 서버가 느리거나 네트워크가 나쁠 때 드러나는 종류이고, 재현을 위해 네트워크를 끊으면 로그인 자체가 막혀 홈에 도달하지 못한다. 그래서 화면 단 가드 테스트를 증거로 삼았다 — 위 실패 재현이 «로딩 분기가 false 를 넘기면 미완료 배지가 그려진다» 를 그대로 보여 준다.

🤖 Generated with Claude Code

로딩 분기가 isRecipientDesignated 를 false 로 고정해, 아직 조회되지 않은 상태를
«미완료» 라는 결과로 단정했다. 이미 지정한 사용자도 진입할 때마다 그 배지를 보고,
배지가 탭 가능한 CTA 라 하지 않아도 될 행동을 유도했다. 같은 화면의 오늘의 질문·
카테고리 카운트는 이미 미결정을 스켈레톤으로 표현하고 있어 표현도 어긋나 있었다.

RecipientDesignationBadgeState.Unknown 을 두고 화면은 Boolean? 으로 받는다. 미결정은
shimmer 자리 표시로 그리고 클릭 대상이 되지 않는다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

❌ Screenshot test fail — baseline 과 CI rendered PNG 차이.

📦 Artifact: https://github.com/Afternote/Afternote-FE/actions/runs/32708333533 (페이지 하단 Artifacts → screenshot-test-results)

🔍 확인 위치 (artifact zip 내부):

  • diff PNG: preview/debug/diffs/...
  • rendered PNG: preview/debug/rendered/... (CI 환경에서 새로 그린 결과)
  • baseline PNG: repo <module>/src/screenshotTestDebug/reference/... (:core:ui · :feature:home:presentation · :feature:receiver:presentation · :feature:onboarding:presentation · :feature:afternote:presentation)

📝 조치 가이드:

  • 의도된 시각 변경 → docker build -t afternote-screenshot:latest -f Dockerfile.screenshot . 후 docker run --rm -v "$PWD":/workspace -w /workspace afternote-screenshot:latest ./gradlew :<module>:updateScreenshotTest 로 baseline 갱신 + commit/push
  • diff 가 anti-aliasing/font 차이로 보이면 docker 환경 변경 여부 (Dockerfile.screenshot) 점검

CI ↔ 로컬 환경 차이는 본 워크플로의 container 단계가 root fix 한다 (README 의 "Compose Preview Screenshot Testing (docker baseline)" 섹션 참조).

@OnlyReadOneBook OnlyReadOneBook left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

총평

«조회 전을 결과로 확정하지 않는다» 는 방향과 Unknown 상태 신설, 화면 배선까지 함께 보는 테스트 설계는 좋습니다. 다만 그 상태가 화면에 그려지는 결과가 의도와 반대입니다 — 아래 1번이 blocking 입니다.

지적

  1. [blocking] 미결정 배지가 문구를 그대로 노출하고 shimmer 는 보이지 않는다 → core/ui/src/main/kotlin/com/afternote/core/ui/badge/RecipientDesignationBadge.kt:56 인라인.
  2. [blocking] 스크린샷 baseline 미갱신 — CI red. 본문에 적어 두신 대로 Validate Compose Preview Screenshots 가 실패 중입니다. 1번을 고치면 어차피 렌더가 또 바뀌므로, 렌더를 확정한 뒤 한 번에 갱신하는 편이 낫겠습니다. Docker 가 없어 못 하신 거라면 제가 대신 돌려도 됩니다 — 말씀 주세요.
  3. [normal] Boolean? 로 3-상태를 표현한다 → feature/home/presentation/src/main/kotlin/com/afternote/feature/home/presentation/HomeTabScreen.kt:207 인라인.
  4. [normal] 문자열 리소스 주석이 사실과 다르다 → core/ui/src/main/res/values/strings.xml:21 인라인.

검증

  • 실기동 QA: 렌더 실측으로 대체. 본문에 적으신 대로 로딩 창이 덤프 간격보다 짧아 에뮬레이터로는 잡히지 않습니다. 대신 이 PR 의 CI 스크린샷 아티팩트(run 32708333533, screenshot-test-results)에서 homeTabScreenLoadingScreenshot 의 실제 렌더를 받아 확인했습니다 — 1번의 근거가 그 이미지입니다.
  • 시안 대조: 부분 수행. «조회 전» 상태의 시안 노드가 이슈·PR 어디에도 없어 대조할 정본이 없습니다. 다만 같은 화면 안의 기존 관행과는 대조했습니다 — 오늘의 질문·카테고리 카운트는 빈 Box 에 shimmer 만 거는 형태(TodayQuestionCard.kt:150, RecordCategoryCard.kt:134)이고, 이 배지만 문구가 남습니다. 시안 부재는 블로커로 세지 않았습니다.
  • CI: 스크린샷 외 전 체크 green.

checkboxState = CheckboxState.None,
showTrailingArrow = false,
onClick = null,
modifier = modifier.shimmerLoadingPlaceholder(),

@OnlyReadOneBook OnlyReadOneBook Aug 24, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[blocking] 이 조합은 48번 줄 주석(«문구 없이 자리만 잡는다»)과 정반대로 그려집니다. 실제 렌더로 확인했습니다 — 회색 칩에 ○ 수신인 지정 여부 확인 중 이 그대로 보이고, shimmer 애니메이션은 흔적도 없습니다.

원인은 두 겹입니다.

1) shimmer 가 배경보다 아래에 그려진다. shimmerLoadingPlaceholder 는 drawRect(brush) → drawContent() 순입니다 (ShimmerModifier.kt:74). 이 modifier 를 칩에 넘기면 칩 내부에서 modifier.border(...).clip(shape).background(backgroundColor) 로 이어지므로, 불투명한 gray2(0xFFEEEEEE) 가 shimmer 를 통째로 덮습니다. 그래서 정지된 회색 칩이 됩니다.

2) 그 위에 label 이 그대로 그려진다. CircularCheckboxOutlineChip 은 Text(label, color = gray9) 를 항상 렌더합니다 (CircularCheckboxOutlineChip.kt:64). shimmer 로 «덮는다» 는 전제가 성립하지 않습니다.

결과적으로 이 PR 이 없애려던 문제(«조회 전인데 확정된 무언가로 읽힘»)가 문구만 바뀐 채 남습니다. 게다가 시안에 없는 «수신인 지정 여부 확인 중» 이라는 안내가 홈 첫 화면에 노출됩니다.

레포의 기존 shimmer 관행은 콘텐츠를 그리지 않고 빈 Box 에만 거는 것입니다.

// TodayQuestionCard.kt:150 · RecordCategoryCard.kt:134
Box(modifier = Modifier.width(...).height(...).clip(RoundedCornerShape(4.dp)).shimmerLoadingPlaceholder())

같은 형태로 칩 크기의 자리만 잡고, TalkBack 용 문구는 Modifier.semantics { contentDescription = ... } 로 주는 편이 의도한 결과와 맞습니다.


실측 — 이 PR 의 CI 스크린샷 아티팩트(run 32708333533, screenshot-test-results)에서 받은 homeTabScreenLoadingScreenshot 실제 렌더입니다.

로딩 배지 확대

체크박스 ○ 와 «수신인 지정 여부 확인 중» 이 그대로 보이고, 배경은 shimmer 그라데이션이 아니라 균일한 gray2 입니다. 바로 아래 TODAY'S QUESTION 스켈레톤(문구 없는 빈 바)이 레포의 기존 shimmer 관행이고, 이 배지만 다릅니다.

로딩 화면 전체

Comment thread core/ui/src/main/res/values/strings.xml Outdated
<string name="core_ui_recipient_designated">수신인 지정 완료</string>
<string name="core_ui_recipient_not_designated">수신인 지정 미완료</string>
<!--
조회 전 자리 표시. shimmer 가 덮으므로 문구 자체는 보이지 않지만, 폭을 잡고

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[normal] «shimmer 가 덮으므로 문구 자체는 보이지 않지만» 이 사실과 다릅니다 — 실제로는 그대로 보입니다(근거는 RecipientDesignationBadge.kt:56 인라인).

렌더 방식을 고치면서 이 주석도 함께 정정해 주세요. 지금은 다음 사람이 «안 보이는 문자열이니 아무거나 써도 된다» 고 읽을 여지가 있습니다.

userName: String,
isRecipientDesignated: Boolean,
/** null 이면 아직 조회 전 — 배지를 어느 쪽으로도 확정하지 않는다 (#698). */
isRecipientDesignated: Boolean?,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[normal] 세 상태(미결정·완료·미완료)를 표현할 타입이 이미 있는데(RecipientDesignationBadgeState), 여기서는 Boolean? 으로 한 번 좁혔다가 236번 줄에서 when 으로 다시 펼칩니다. null 이 «미결정» 이라는 건 주석으로만 유지되는 약속입니다.

0824 에 정한 «널+폴백 대신 값으로 명시» 방향(#934)과도 어긋납니다. 파라미터를 badgeState: RecipientDesignationBadgeState 로 받으면 변환이 사라지고 호출부(로딩/성공 분기)에서 의도가 그대로 드러납니다.

Incomplete 의 onClick 배선이 이 함수 안에 있어야 한다면 그것만 별도 파라미터로 빼도 되고, 지금처럼 두더라도 최소한 «null = 미결정» 을 타입 이름으로 표현할 방법을 한 번 검토해 주세요.

homeTabScreenLoadingScreenshot 이 HomeTabUiState.Loading 을 그린다 — 미완료 배지가
shimmer 자리 표시로 바뀐 의도된 시각 변경이다.

CI 와 같은 docker 컨테이너(Dockerfile.screenshot)에서 생성했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Sadturtleman

Copy link
Copy Markdown
Collaborator Author

스크린샷 baseline 갱신했습니다 — 같은 docker 컨테이너에서 :feature:home:presentation:updateScreenshotTest 를 돌렸고, 바뀐 PNG 는 homeTabScreenLoadingScreenshot 한 장뿐입니다(미완료 배지 → shimmer 자리 표시).

Sadturtleman and others added 2 commits August 24, 2026 22:22
칩에 shimmer 를 걸면 칩 내부의 불투명 배경이 shimmer 를 덮어 정지된 회색 칩이 되고,
라벨은 그 위에 그대로 그려진다 — «조회 전인데 확정된 무언가로 읽힘» 이 문구만 바뀐 채
남고, 시안에 없는 안내가 홈 첫 화면에 노출된다.

같은 화면의 오늘의 질문·카테고리 카운트와 같은 형태로 빈 Box 에만 shimmer 를 걸고,
읽을 것은 semantics 로 준다.

화면 파라미터도 Boolean? 대신 RecipientDesignationBadgeState 로 받는다 — «null =
미결정» 이 주석으로만 유지되는 약속이 되지 않게 한다.

리뷰 지적.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
칩 대신 콘텐츠 없는 shimmer 자리 표시로 바뀐 의도된 시각 변경이다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Sadturtleman

Copy link
Copy Markdown
Collaborator Author

지적 4건 반영했습니다. 1번은 CI 아티팩트에서 실제 렌더를 받아 짚어 주신 덕에 확인했습니다 — shimmer 가 배경 아래에 그려지고 라벨이 그대로 남는다는 진단이 정확합니다.

1. [blocking] 렌더가 의도와 반대 — 칩을 쓰지 않고, 레포 관행대로 빈 Box 에만 shimmer 를 겁니다. 읽을 것은 semantics { contentDescription } 로 줍니다.

Box(
    modifier = modifier.width(...).height(...).clip(RoundedCornerShape(20.dp))
        .shimmerLoadingPlaceholder()
        .semantics { contentDescription = unknownDescription },
)

테스트도 «문구를 그리지 않는다 + 접근성으로만 읽힌다» 로 바꿨습니다.

2. [blocking] baseline — 1번으로 렌더가 확정된 뒤 갱신했습니다(같은 docker 컨테이너). homeTabScreenLoadingScreenshot 한 장입니다.

3. [normal] Boolean? 로 3-상태 — 화면 파라미터를 RecipientDesignationBadgeState 로 바꿨습니다. 변환이 사라지고 로딩/성공 분기에서 의도가 그대로 드러납니다. #934 방향과도 맞습니다.

4. [normal] 문자열 주석이 사실과 다름 — «화면에는 그리지 않고 TalkBack 에만 읽힌다» 로 정정했습니다. 1번을 고치면서 이제 실제로 그렇게 됩니다.

@OnlyReadOneBook OnlyReadOneBook left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

총평

지적 4건 전부 반영됐습니다. 특히 1번은 «shimmer 를 칩에 걸면 칩 내부의 불투명 배경이 그것을 덮는다» 는 구조를 정확히 짚어, 칩을 shimmer 로 덮으려 하는 대신 콘텐츠를 아예 그리지 않는 형태로 바꾸셨습니다. 같은 화면의 오늘의 질문·카테고리 카운트와 형태가 맞고, 읽을 것은 semantics 로 남겨 TalkBack 도 유지됩니다. 승인합니다.

반영 확인

  1. [blocking] 해소. Box + clip(RoundedCornerShape(20.dp)) + shimmerLoadingPlaceholder() + semantics { contentDescription }. 칩의 shape(CircularCheckboxOutlineChip.kt:42)과 같은 20.dp 라 모양도 어긋나지 않습니다. 갱신된 baseline 이미지로 실제 렌더를 확인했습니다 — 인사말 아래 배지 자리가 문구 없이 회색 자리표시자로만 그려지고, 오늘의 질문 카드의 placeholder 두 줄과 같은 형태입니다. 지난번 지적한 «회색 칩에 문구가 그대로 보이는» 상태는 사라졌습니다.
  2. [blocking] 해소. homeTabScreenLoadingScreenshot baseline 갱신됨. 렌더를 두 번 바꾸신 만큼 baseline 도 두 번 갱신하셨고, 최종 상태 기준으로 스크린샷 체크가 green 입니다.
  3. [normal] 해소. Boolean? → RecipientDesignationBadgeState 로 파라미터 타입 자체를 바꿔, 좁혔다 when 으로 다시 펼치던 왕복이 없어졌습니다. 호출부에서 로딩=Unknown, 성공=Completed/Incomplete 로 의도가 그대로 드러납니다.
  4. [normal] 해소. 문자열 주석이 «화면에는 그리지 않고 TalkBack 에만 읽힌다» 로 정정됐습니다. 사실과 일치합니다.

테스트도 assertDoesNotExist() + onNodeWithContentDescription 로 «문구는 안 그리되 접근성에는 남는다» 를 정확히 고정했습니다.

남는 것 (blocking 아님, 후속으로 보셔도 됩니다)

  • [nit] 자리표시자 크기가 고정 dp → core/ui/src/main/kotlin/com/afternote/core/ui/badge/RecipientDesignationBadge.kt:119 인라인.
  • [nit] Unknown 상태만 @Preview 가 없습니다. 완료·미완료 두 프리뷰는 있는데, 이번에 문제가 됐던 바로 그 상태에는 프리뷰가 없어 core:ui 자체 baseline 이 커버하지 않습니다(홈 화면 baseline 이 간접적으로만 잡습니다). 프리뷰를 하나 더 두면 같은 회귀가 컴포넌트 단위에서 눈에 띕니다.

검증

  • CI: head 555a352d 기준 13개 체크 전부 SUCCESS — Validate Compose Preview Screenshots 포함.
  • 렌더 실측: 위 1번대로 갱신된 baseline PNG 를 직접 열어 확인했습니다.
  • develop 접점: merge-tree 충돌 0건. merge-base 이후 develop 이 건드린 파일과 이 PR 이 건드린 파일의 교집합도 0건이라, 텍스트·의미 어느 쪽으로도 드리프트가 없습니다. (브랜치가 develop 최신을 아직 안 물고 있지만 충돌 요인이 없어 그대로 머지 가능합니다.)
  • 시안 대조: 지난번과 같은 사정 — 근거 부재로 미수행, 블로커 아님. «조회 전» 상태의 시안 노드가 여전히 없습니다. 다만 이번 형태는 같은 화면의 기존 로딩 관행(오늘의 질문·카테고리 카운트의 빈 자리 + shimmer)과 정확히 일치해, 화면 내 일관성 근거로는 충분합니다.
  • 참고(이 PR 밖): 갱신된 로딩 baseline 우하단에 THIS WEEK 기록된 순간들 = 7 이 그대로 찍혀 있습니다. #562·#994 가 지금 걷어내는 중인 그 자리표시자라, 이 PR 이 책임질 몫은 아닙니다 — 그쪽이 머지되면 이 baseline 도 함께 바뀝니다.

}

/** 확정 상태 칩과 같은 자리를 차지하도록 맞춘 크기 (`CircularCheckboxOutlineChip` 실측). */
private val UNKNOWN_BADGE_WIDTH = 132.dp

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] 확정 상태 칩은 고정 크기가 아니라 콘텐츠 wrap + padding(horizontal = 15.dp, vertical = 9.dp) 입니다(CircularCheckboxOutlineChip.kt:55). 그래서 이 132×36 은 «지금 이 폰트 크기에서 잰 한 시점의 값» 입니다.

두 가지가 걸립니다.

  1. 완료(수신인 지정 완료)와 미완료(수신인 지정 미완료 + 후행 화살표)의 폭이 서로 다른데, 자리표시자는 하나의 고정값이라 어느 쪽으로 전환돼도 폭이 한 번 튑니다.
  2. 폰트 스케일을 키우면 칩만 커지고 자리표시자는 그대로입니다. 접근성 설정에서 글꼴을 키운 사용자에게는 자리표시자가 실제 칩보다 눈에 띄게 작아집니다.

지금 값이 틀렸다는 건 아니고, 칩 라벨이 바뀌거나 타이포가 조정되면 조용히 어긋나는 종류라 «실측값 상수» 로 굳히기보다 칩과 같은 계산을 타게 하는 편이 안전합니다. 예를 들어 확정 상태 칩을 alpha = 0f 로 그려 자리만 차지하게 하면 padding·타이포·폰트 스케일이 자동으로 따라옵니다.

후속으로 보셔도 됩니다 — 승인 자체를 막을 사안은 아닙니다.

@Sadturtleman
Sadturtleman merged commit a079724 into develop Aug 24, 2026
13 checks passed
@Sadturtleman
Sadturtleman deleted the fix/698-recipient-badge-loading branch August 24, 2026 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(home): 로딩 중 수신인 지정 배지가 '미완료' 로 확정 표시 — 미결정을 결과로 단정

2 participants