Repository navigation
fix(home): 조회 전 수신인 지정 배지를 '미완료' 로 확정하지 않는다 (#698) - #1021
Conversation
로딩 분기가 isRecipientDesignated 를 false 로 고정해, 아직 조회되지 않은 상태를 «미완료» 라는 결과로 단정했다. 이미 지정한 사용자도 진입할 때마다 그 배지를 보고, 배지가 탭 가능한 CTA 라 하지 않아도 될 행동을 유도했다. 같은 화면의 오늘의 질문· 카테고리 카운트는 이미 미결정을 스켈레톤으로 표현하고 있어 표현도 어긋나 있었다. RecipientDesignationBadgeState.Unknown 을 두고 화면은 Boolean? 으로 받는다. 미결정은 shimmer 자리 표시로 그리고 클릭 대상이 되지 않는다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
❌ Screenshot test fail — baseline 과 CI rendered PNG 차이. 📦 Artifact: https://github.com/Afternote/Afternote-FE/actions/runs/32708333533 (페이지 하단 Artifacts → 🔍 확인 위치 (artifact zip 내부):
📝 조치 가이드:
CI ↔ 로컬 환경 차이는 본 워크플로의 container 단계가 root fix 한다 (README 의 "Compose Preview Screenshot Testing (docker baseline)" 섹션 참조). |
OnlyReadOneBook
left a comment
There was a problem hiding this comment.
총평
«조회 전을 결과로 확정하지 않는다» 는 방향과 Unknown 상태 신설, 화면 배선까지 함께 보는 테스트 설계는 좋습니다. 다만 그 상태가 화면에 그려지는 결과가 의도와 반대입니다 — 아래 1번이 blocking 입니다.
지적
- [blocking] 미결정 배지가 문구를 그대로 노출하고 shimmer 는 보이지 않는다 →
core/ui/src/main/kotlin/com/afternote/core/ui/badge/RecipientDesignationBadge.kt:56인라인. - [blocking] 스크린샷 baseline 미갱신 — CI red. 본문에 적어 두신 대로
Validate Compose Preview Screenshots가 실패 중입니다. 1번을 고치면 어차피 렌더가 또 바뀌므로, 렌더를 확정한 뒤 한 번에 갱신하는 편이 낫겠습니다. Docker 가 없어 못 하신 거라면 제가 대신 돌려도 됩니다 — 말씀 주세요. - [normal]
Boolean?로 3-상태를 표현한다 →feature/home/presentation/src/main/kotlin/com/afternote/feature/home/presentation/HomeTabScreen.kt:207인라인. - [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(), |
There was a problem hiding this comment.
[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 관행이고, 이 배지만 다릅니다.
| <string name="core_ui_recipient_designated">수신인 지정 완료</string> | ||
| <string name="core_ui_recipient_not_designated">수신인 지정 미완료</string> | ||
| <!-- | ||
| 조회 전 자리 표시. shimmer 가 덮으므로 문구 자체는 보이지 않지만, 폭을 잡고 |
There was a problem hiding this comment.
[normal] «shimmer 가 덮으므로 문구 자체는 보이지 않지만» 이 사실과 다릅니다 — 실제로는 그대로 보입니다(근거는 RecipientDesignationBadge.kt:56 인라인).
렌더 방식을 고치면서 이 주석도 함께 정정해 주세요. 지금은 다음 사람이 «안 보이는 문자열이니 아무거나 써도 된다» 고 읽을 여지가 있습니다.
| userName: String, | ||
| isRecipientDesignated: Boolean, | ||
| /** null 이면 아직 조회 전 — 배지를 어느 쪽으로도 확정하지 않는다 (#698). */ | ||
| isRecipientDesignated: Boolean?, |
There was a problem hiding this comment.
[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>
|
스크린샷 baseline 갱신했습니다 — 같은 docker 컨테이너에서 |
칩에 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>
|
지적 4건 반영했습니다. 1번은 CI 아티팩트에서 실제 렌더를 받아 짚어 주신 덕에 확인했습니다 — shimmer 가 배경 아래에 그려지고 라벨이 그대로 남는다는 진단이 정확합니다. 1. [blocking] 렌더가 의도와 반대 — 칩을 쓰지 않고, 레포 관행대로 빈 Box 에만 shimmer 를 겁니다. 읽을 것은 Box(
modifier = modifier.width(...).height(...).clip(RoundedCornerShape(20.dp))
.shimmerLoadingPlaceholder()
.semantics { contentDescription = unknownDescription },
)테스트도 «문구를 그리지 않는다 + 접근성으로만 읽힌다» 로 바꿨습니다. 2. [blocking] baseline — 1번으로 렌더가 확정된 뒤 갱신했습니다(같은 docker 컨테이너). 3. [normal] 4. [normal] 문자열 주석이 사실과 다름 — «화면에는 그리지 않고 TalkBack 에만 읽힌다» 로 정정했습니다. 1번을 고치면서 이제 실제로 그렇게 됩니다. |
OnlyReadOneBook
left a comment
There was a problem hiding this comment.
총평
지적 4건 전부 반영됐습니다. 특히 1번은 «shimmer 를 칩에 걸면 칩 내부의 불투명 배경이 그것을 덮는다» 는 구조를 정확히 짚어, 칩을 shimmer 로 덮으려 하는 대신 콘텐츠를 아예 그리지 않는 형태로 바꾸셨습니다. 같은 화면의 오늘의 질문·카테고리 카운트와 형태가 맞고, 읽을 것은 semantics 로 남겨 TalkBack 도 유지됩니다. 승인합니다.
반영 확인
- [blocking] 해소.
Box+clip(RoundedCornerShape(20.dp))+shimmerLoadingPlaceholder()+semantics { contentDescription }. 칩의 shape(CircularCheckboxOutlineChip.kt:42)과 같은 20.dp 라 모양도 어긋나지 않습니다. 갱신된 baseline 이미지로 실제 렌더를 확인했습니다 — 인사말 아래 배지 자리가 문구 없이 회색 자리표시자로만 그려지고, 오늘의 질문 카드의 placeholder 두 줄과 같은 형태입니다. 지난번 지적한 «회색 칩에 문구가 그대로 보이는» 상태는 사라졌습니다. - [blocking] 해소.
homeTabScreenLoadingScreenshotbaseline 갱신됨. 렌더를 두 번 바꾸신 만큼 baseline 도 두 번 갱신하셨고, 최종 상태 기준으로 스크린샷 체크가 green 입니다. - [normal] 해소.
Boolean?→RecipientDesignationBadgeState로 파라미터 타입 자체를 바꿔, 좁혔다when으로 다시 펼치던 왕복이 없어졌습니다. 호출부에서 로딩=Unknown, 성공=Completed/Incomplete로 의도가 그대로 드러납니다. - [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 |
There was a problem hiding this comment.
[nit] 확정 상태 칩은 고정 크기가 아니라 콘텐츠 wrap + padding(horizontal = 15.dp, vertical = 9.dp) 입니다(CircularCheckboxOutlineChip.kt:55). 그래서 이 132×36 은 «지금 이 폰트 크기에서 잰 한 시점의 값» 입니다.
두 가지가 걸립니다.
- 완료(
수신인 지정 완료)와 미완료(수신인 지정 미완료+ 후행 화살표)의 폭이 서로 다른데, 자리표시자는 하나의 고정값이라 어느 쪽으로 전환돼도 폭이 한 번 튑니다. - 폰트 스케일을 키우면 칩만 커지고 자리표시자는 그대로입니다. 접근성 설정에서 글꼴을 키운 사용자에게는 자리표시자가 실제 칩보다 눈에 띄게 작아집니다.
지금 값이 틀렸다는 건 아니고, 칩 라벨이 바뀌거나 타이포가 조정되면 조용히 어긋나는 종류라 «실측값 상수» 로 굳히기보다 칩과 같은 계산을 타게 하는 편이 안전합니다. 예를 들어 확정 상태 칩을 alpha = 0f 로 그려 자리만 차지하게 하면 padding·타이포·폰트 스케일이 자동으로 따라옵니다.
후속으로 보셔도 됩니다 — 승인 자체를 막을 사안은 아닙니다.
Closes #698
무엇이 문제였나
홈 로딩 분기가
isRecipientDesignated = false를 넘겨 아직 조회되지 않은 상태를 「미완료」라는 결과로 단정했다. 이미 수신인을 지정한 사용자도 진입 동안 그 배지를 보고, 배지가 탭 가능한 CTA(Incomplete) 라 하지 않아도 될 행동을 유도했다.같은 분기에서 오늘의 질문은 시머 스켈레톤, 카테고리 카운트는
categoryCountsLoading = true로 미결정을 미결정으로 표현한다. 배지만 결과를 확정해 표현이 어긋나 있었다.무엇을 했나
이슈의 두 가지 요구를 그대로 따랐다.
RecipientDesignationBadgeState.Unknown신설 — 문구 없이 자리만 잡고 shimmer 로 «아직 오는 중» 을 알린다. 클릭 대상이 아니다HomeTabScrollContent(isRecipientDesignated: Boolean?)— 로딩 분기는null을 넘긴다.false를 확정값처럼 흘리지 않는다TalkBack 이 읽을 것이 없으면 화면에 무슨 일이 있는지 알 수 없으므로 자리 표시 문구(
수신인 지정 여부 확인 중)를 두되, 확정 문구("완료"·"미완료")는 쓰지 않는다.회귀 가드
RecipientBadgeLoadingTest4개 — Robolectric + Compose. shimmer 가 무한 애니메이션이라 Compose 가 영원히 idle 이 되지 않으므로mainClock.autoAdvance = false로 한 프레임만 그려 읽는다.컴포넌트만이 아니라 화면 배선까지 본다. 컴포넌트에
Unknown을 두고도 화면이 여전히false를 넘기면 증상이 그대로 남기 때문이다.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:updateScreenshotTestdiff 는 «로딩 화면의 미완료 배지가 shimmer 자리 표시로 바뀐» 한 장이어야 한다.
core:ui의 배지 baseline 2종(Completed·Incomplete)은 렌더가 그대로라 영향이 없다 —Unknown용 preview 는 baseline 을 만들 수 없어 추가하지 않았다.실기기 관찰
에뮬레이터에서 진입 직후를 8회 연속 덤프했는데 로딩 창이 덤프 간격(~0.6s)보다 짧아 고치기 전 빌드에서도 「미완료」가 잡히지 않았다. 즉 이 결함은 서버가 느리거나 네트워크가 나쁠 때 드러나는 종류이고, 재현을 위해 네트워크를 끊으면 로그인 자체가 막혀 홈에 도달하지 못한다. 그래서 화면 단 가드 테스트를 증거로 삼았다 — 위 실패 재현이 «로딩 분기가 false 를 넘기면 미완료 배지가 그려진다» 를 그대로 보여 준다.
🤖 Generated with Claude Code