[SBISSUE-21901] fix: Keep modal content clear of the Android navigation bar - #313
[SBISSUE-21901] fix: Keep modal content clear of the Android navigation bar#313OnestarLee wants to merge 6 commits into
Conversation
…21901) On Android with edge-to-edge enforcement the long-press message action BottomSheet rendered its last item underneath the system navigation bar, where it was partially hidden and un-tappable. Reproduced on a Pixel 9 Pro API 36 emulator with 3-button navigation. Two things went wrong together: - A modal is presented in its own native window. `Modal` left that window out of edge-to-edge, so the window decor inset the modal content view while the JS layout kept spanning the whole window, and the content overflowed behind the navigation bar. - The sheets read their safe area insets from a component rendered above the modal, so they got the insets of the view that hosts the app. When the app has already consumed the bottom inset above SendbirdUIKitContainer, that value is `0` and no inset was reserved. `Modal` now matches the app window mode and nests a `SafeAreaProvider` so its content measures the insets of its own window, and the bottom anchored sheets read the insets inside the modal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #313 +/- ##
==========================================
- Coverage 11.35% 11.34% -0.02%
==========================================
Files 360 361 +1
Lines 9094 9105 +11
Branches 2563 2585 +22
==========================================
Hits 1033 1033
+ Misses 8060 7996 -64
- Partials 1 76 +75 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
OnestarLee
left a comment
There was a problem hiding this comment.
세로 모드의 보고된 재현 조건은 정상적으로 해결되는 것을 확인했습니다. 다만 가로 모드에서 내비게이션 바가 측면으로 이동하는 경우에는 아래 두 경로가 남아 있어 확인 부탁드립니다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Detect the edge-to-edge window from the bottom, left and right insets. In landscape the navigation inset moves to the side, so a bottom-only check left the modal window mode out of sync with the app window. - Turn the safe area helper into `ModalSafeArea`, which supplies every edge to its children instead of only padding the bottom. The reaction sheets read the horizontal insets outside the modal before, so a side navigation bar could cover their last column. - Seed the nested provider with `initialWindowMetrics` so the modal content never waits for the first native measurement. `BottomSheet` and the user profile card now go through the same helper, so every modal reads its insets one way.
bang9
left a comment
There was a problem hiding this comment.
LGTM 👍 실기 검증 결과를 정리해서 올립니다. 티켓의 근본 원인과 이 PR의 수정 효과는 재현으로 확인했습니다. IS_EDGE_TO_EDGE_WINDOW 판정식 하나만 의도를 확인하고 싶은데, 동작상 문제는 아니라 머지를 막을 사안은 아닙니다.
TL;DR
- ✅ SBISSUE-21901 증상을
main에서 재현하고, 이 브랜치에서 해소되는 것을 4개 모달 표면에서 픽셀 단위로 확인했습니다. - ✅ 기본 조건(호스트 앱이 inset을 소비하지 않는 sample)에서는 회귀 없습니다. VoiceMessageInput은 이중 padding 126px이 제거되어 오히려 개선됩니다.
⚠️ IS_EDGE_TO_EDGE_WINDOW는 edge-to-edge를 판정하지 못합니다. 사실상 "Android + nav bar 존재"와 동의어라서 e2e가 꺼진 앱에서도 항상true입니다.- ℹ️ 실제로 버그를 고치는 것은 중첩
SafeAreaProvider이고,navigationBarTranslucent는 nav bar 배경 처리(시각적 효과)만 담당합니다.
1. 근본 원인
useSafeAreaPadding이 참조하는 provider는 SendbirdUIKitContainer의 root SafeAreaProvider이고, 이 provider는 자신의 뷰(액티비티 계층) 를 측정합니다. RN Modal은 별도 네이티브 윈도우(Dialog)이므로 React context는 경계를 넘어가지만 측정 대상은 넘어가지 않습니다.
react-native-safe-area-context 5.6.2 SafeAreaUtils.kt:
bottom = max(min(visibleRect.top + view.height - windowHeight, 0f) + windowInsets.bottom, 0f)호스트 앱이 UIKit 위에서 bottom inset을 이미 소비한 경우(자체 SafeAreaView edges={['bottom']}, 하단 탭바, expo-router layout 등) root provider의 뷰는 화면 하단에 닿지 않으므로 위 식이 0을 돌려줍니다. 반면 모달 윈도우는 여전히 nav bar 뒤까지 full-bleed이므로 paddingBottom: 0이 되어 마지막 항목이 nav bar 아래로 들어갑니다.
티켓 첨부 스크린샷도 정확히 이 상태입니다 — 시트 배경이 화면 맨 아래까지 도달하고 Reply in thread가 nav bar band 안에 통째로 들어가 있습니다. 그리고 티켓 환경(Expo SDK 55, edge-to-edge 강제)에서는 isEdgeToEdgeFeatureFlagOn이 이미 true라 RN 쪽 getter가 field || isEdgeToEdgeFeatureFlagOn이므로 navigationBarTranslucent prop은 no-op입니다. 즉 이 티켓을 실제로 고치는 것은 중첩 provider 하나입니다.
재현을 위해 sample 앱에 아래 조건만 임시로 넣었습니다.
<SafeAreaProvider>
<SafeAreaView edges={['bottom']} style={{ flex: 1 }}>
<SendbirdUIKitContainer ...>{/* ... */}</SendbirdUIKitContainer>
</SafeAreaView>
</SafeAreaProvider>2. 수정 검증 — 재현 조건에서 main vs branch
Pixel 2 / API 36 / OS 강제 edge-to-edge(Gradle 속성 없음) / 3버튼 내비 48dp = 126px, nav bar band 1794..1920.
모든 이미지에서 빨간 선 = nav bar 상단 y=1794, 왼쪽 = main(7eb5bf07 + 재현 패치, JS 번들 0be5934288f7), 오른쪽 = fix/bottomsheet-navbar-inset(3fc04cd3 + 동일 패치, JS 번들 2ebf7323d3fb)입니다.
액션 시트 — 왼쪽
main:Reply in thread행이 opaque nav bar 안에 통째로 들어가 back/home 아이콘이 텍스트 위에 겹칩니다(터치도 nav bar가 먹습니다). 오른쪽 branch: 빨간 선 위로 완전히 올라옵니다.
첨부 시트 — 동일 증상, 동일 해소.
프로필 카드 — 왼쪽
main: user ID가 nav bar 아이콘 밑에 깔립니다.
음성 메시지 입력 — 이 조건에서는 양쪽 동일합니다.
SendInput의Modal이statusBarTranslucent를 넘기지 않아 모달 윈도우 자체가 nav bar 위에서 잘리기 때문에 원래 가려지지 않습니다.
측정값(uiautomator bounds, 행 높이 = DialogSheet.Item 48dp × createStyleSheet 스케일 1.143 × density 2.625 = 144px):
| 표면 | main 마지막 항목 | branch 마지막 항목 | 행 하단 (main → branch) | 적용된 paddingBottom |
|---|---|---|---|---|
| 액션 시트 | Reply in thread 1815..1881 |
1689..1755 |
1920 → 1794 | 0 → 126 (= nav bar 높이) |
| 첨부 시트 | Files 1815..1881 |
1689..1755 |
1920 → 1794 | 0 → 126 |
| 프로필 카드 | user ID 1812..1872 |
1686..1746 |
— | 0 → 126 |
| 음성 메시지 입력 | Cancel 1662..1711 |
Cancel 1662..1711 |
변화 없음 | 변화 없음 |
branch에서 행 하단이 nav bar 상단(1794)에 정확히 맞는다는 점이, 중첩 provider가 액티비티가 아니라 모달 윈도우를 측정하고 있다는 직접 증거입니다.
3. 회귀 검증 — 기본 sample(호스트가 inset 미소비)
호스트가 inset을 소비하지 않으면 root provider의 값과 모달 윈도우의 값이 우연히 일치하므로 main에도 문제가 없습니다. 이 조건에서 변경으로 인한 회귀가 없는지 확인했습니다. 아래는 모두 왼쪽 = main 7eb5bf07(번들 c13f74139bd2), 오른쪽 = fix/bottomsheet-navbar-inset 3fc04cd3(번들 b7f54c4244eb)입니다.
프로필 카드 / API 36 — 콘텐츠 y는 완전히 동일하고 카드 상단 흰 밴드만 33px 내려갑니다(카드가 33px 짧아짐). 하단은 양쪽 모두 여유가 남습니다. 눈에 보이는 차이는 branch에서 nav bar가 translucent가 되어 카드 배경이 아이콘 뒤로 이어지는 것뿐입니다.
음성 메시지 입력 / API 36 — 여기는 버그가 하나 고쳐집니다.
main은Cancel/녹음 버튼 행 아래로 126px이 빈 흰 공간으로 떠 있습니다.VoiceMessageInput이24 + safeArea.paddingBottom을 적용하는데(f72bb5f, SBISSUE-21340) root provider가 준 126이 이미 nav bar 위에서 잘린 창 안에서 이중 계산되기 때문입니다. branch는 중첩 provider가 이 창의 실제 inset(0)을 읽어서 그 126px이 사라집니다.
액션 시트 / API 36 — 콘텐츠 y 동일(
Reply in thread1689..1755, 행 하단 = nav bar 상단 1794).main은 opaque nav bar가 시트 흰 배경 위에 얹혀 잘려 보이고, branch는 translucent라 배경이 이어집니다. 가려지는 항목은 양쪽 모두 없습니다.
액션 시트 / API 34 +
edgeToEdgeEnabled=true— main/branch 모두Reply in thread1689..1755, 행 하단이 nav bar 상단(1794)에 정확히 맞습니다(paddingBottom = 126 = nav bar 높이). 이 축에서는isEdgeToEdgeFeatureFlagOn이true라 이 PR의 prop이 no-op이므로 예상된 결과입니다.
액션 시트 / API 34 +
edgeToEdgeEnabled=false— 콘텐츠 위치는 양쪽 동일(1689..1755,paddingBottom = 126)하고 가려짐도 없습니다. 다만 nav bar 배경이 main의 불투명 검정에서 branch의 translucent로 바뀝니다. 이 조건은 액티비티가 edge-to-edge가 아닌 케이스(API 34, Gradle 속성 off, OS 강제 없음)인데도 모달에navigationBarTranslucent가 적용됐다는 뜻이고, 아래 4번 항목의 근거입니다.
4. ⚠️ IS_EDGE_TO_EDGE_WINDOW가 edge-to-edge를 판정하지 못합니다
const initialInsets = initialWindowMetrics?.insets;
const IS_EDGE_TO_EDGE_WINDOW =
Platform.OS === 'android' &&
Math.max(initialInsets?.bottom ?? 0, initialInsets?.left ?? 0, initialInsets?.right ?? 0) > 0;initialWindowMetrics.insets의 출처를 따라가면 (SafeAreaContextModule.kt)
val decorView = reactApplicationContext.currentActivity?.window?.decorView as ViewGroup?
val insets = getSafeAreaInsets(decorView) // ← decorView 기준이고, getSafeAreaInsets에서 view === rootView === decorView이므로 view.height == windowHeight가 항상 성립합니다 → min(...) = 0 → bottom = rootWindowInsets.bottom. decorView는 edge-to-edge 여부와 무관하게 항상 화면 전체를 차지하고(inset을 소비하는 것은 android.R.id.content 쪽입니다), rootWindowInsets는 decor fitting과 무관하게 raw inset을 돌려줍니다.
결론: 이 상수는 "Android이고 nav bar/좌우 inset이 존재함"과 동의어이며, e2e가 꺼진 앱에서도 true입니다. 위 3번의 마지막 이미지(API 34 + edgeToEdgeEnabled=false)가 실측 근거입니다 — 액티비티가 e2e가 아닌데도 모달의 nav bar가 translucent로 바뀝니다. 따라서 실질적으로
navigationBarTranslucent={Boolean(statusBarTranslucent)}와 같습니다. 의도를 확인하고 싶습니다.
-
"e2e일 때만 켠다"가 의도였다면 로직이 의도와 다릅니다. e2e가 아닌 호스트 앱의 모든 UIKit 모달이 nav bar 뒤까지 그려지게 됩니다(중첩 provider가 padding을 넣어주므로 콘텐츠는 가려지지 않지만, backdrop/시트 배경이 nav bar 밑으로 확장되는 동작 변경입니다). 진짜 판정이 필요하면
insets가 아니라frame을 봐야 합니다 —getInitialWindowMetrics의frame은android.R.id.content의 프레임이라 e2e일 때만 화면 전체가 됩니다.const m = initialWindowMetrics; const IS_EDGE_TO_EDGE_WINDOW = Platform.OS === 'android' && !!m && m.frame.y + m.frame.height >= Dimensions.get('screen').height;
-
"항상 켠다"가 의도였다면 판정식과 이름을 없애고
navigationBarTranslucent={Boolean(statusBarTranslucent)}로 두고 이유를 주석으로 남기는 편이 읽는 사람에게 정확합니다.
부수적으로, initialWindowMetrics는 모듈 로드 시점 값이라 그 시점에 currentActivity가 없으면(백그라운드 headless 시작 등) null이 되어 이 상수가 앱 생애 동안 false로 굳습니다. 모듈 스코프 상수보다 렌더 시점 계산이 안전합니다.
5. 검증 방법
| 브랜치 | 커밋 | JS 번들 sha | |
|---|---|---|---|
| before | main |
7eb5bf07 |
c13f74139bd2 (기본) / 0be5934288f7 (재현 패치) |
| after | fix/bottomsheet-navbar-inset |
3fc04cd3 |
b7f54c4244eb (기본) / 2ebf7323d3fb (재현 패치) |
JS 번들을 내장한 릴리스 APK로 측정했고, 위 sha로 각 축이 실제 APK에 반영됐음을 확인했습니다. Gradle이 워크스페이스 소스 변경을 추적하지 못해(라이브러리가 node_modules 심볼릭 링크로 해석됨) createBundleReleaseJsAndAssets가 up-to-date로 스킵되며, 강제 재생성하지 않으면 브랜치를 바꿔도 번들이 동일합니다.
측정 축은 세 가지입니다.
| 축 | 기기 | edge-to-edge 경로 | isEdgeToEdgeFeatureFlagOn |
|---|---|---|---|
| A | Pixel 2 / API 36 | OS 강제 (Gradle 속성 없음) | false → 이 PR의 prop이 동작 |
| B | Pixel 2 / API 34 | edgeToEdgeEnabled=true |
true → 이 PR의 prop이 no-op |
| C | Pixel 2 / API 34 | 없음 (edgeToEdgeEnabled=false) |
false |
표면마다 앱을 재시작하고 매 실행마다 새 사용자 ID를 써서 상태 오염을 배제했습니다. 좌표는 uiautomator bounds, 시트/카드 경계는 픽셀 스캔으로 측정했습니다.
기기의 내비게이션 설정은 보고되는 inset과 실제로 그려지는 바가 일치하는지 확인한 뒤 측정했습니다(dumpsys window displays의 app=1080x1794 = bottom inset 126px, 스크린샷의 nav bar band 1794..1920과 일치). AVD에 navbar.gestural과 navbar.threebutton overlay가 동시에 켜져 있으면 앱에는 24dp를 보고하면서 화면에는 48dp 바를 그려서, 시트 마지막 행이 바의 정확히 절반에 걸친 것처럼 보이는 가짜 증상이 나옵니다. 이 값들은 그 상태를 정리한 뒤 재측정한 것입니다.
6. 검증하지 못한 것
ReactionListBottomSheet/ReactionUserListBottomSheet— UI 자동화가 반복 실패해서 수치 미확보- iOS
- 세로/가로 회전 중 상태 변화
initialMetrics가 액티비티 값으로 시드되어 발생할 수 있는 첫 프레임 여백 점프 — 정적 스크린샷으로는 관측 불가
…1901 review) `initialWindowMetrics.insets` is measured from the decor view, which spans the whole screen whether or not the window draws behind the system bars, so the previous check was true for every Android device that has a navigation bar. It turned the modal window edge-to-edge even in apps whose own window is not, which showed up as a translucent navigation bar behind the modal. `frame` is measured from the content view instead, and that view only spans the whole screen when the window is edge-to-edge. Comparing its bottom against the screen height gives the answer the check was meant to give, and reading it during render removes the dependency on module evaluation order.
For Internal Contributors
SBISSUE-21901
Description Of Changes
문제
Android에서 edge-to-edge가 적용된 앱에서 메시지를 길게 눌러 나오는 액션 시트의 마지막 항목이 시스템 내비게이션 바 뒤에 그려져 일부가 가려지고 탭이 되지 않습니다.
제보 환경은
@sendbird/uikit-react-native3.12.6, react-native 0.83.6, Expo SDK 55(edge-to-edge 강제), Pixel 9 Pro, Android 17(targetSdk 35), 3버튼 내비게이션입니다.재현 조건
Pixel 9 Pro API 36 에뮬레이터에 3버튼 내비게이션을 켜고 확인했습니다. edge-to-edge만으로는 재현되지 않고, 호스트 앱이
SendbirdUIKitContainer보다 위에서 하단 여백을 이미 소비한 경우에만 발생합니다. 예를 들어 컨테이너를SafeAreaView edges={['bottom']}로 감싸거나, 커스텀 하단 탭 바나 화면 레이아웃이 하단 여백을 차지하는 구성입니다.밀도 3.0 화면에서 픽셀로 측정한 값입니다. 시트가 하단 여백을
0px만 확보해서, 마지막 항목171px중144px이 내비게이션 바(y=2712부터) 뒤로 들어갔습니다.원인
각각은 그 자체로 문제가 없지만, 두 가지가 겹칠 때 잘림이 생깁니다.
1. 모달 창이 edge-to-edge가 아니었습니다.
모달은 자기만의 네이티브 창에 표시됩니다.
Modal이navigationBarTranslucent를 넘기지 않아서 창 decor가 모달 내용 뷰를904dp로 깎는데, JS 레이아웃은 창 전체인952dp를 그대로 사용합니다. 그래서 시트가 내비게이션 바 뒤로 흘러넘칩니다.2. 시트가 여백을 모달 밖에서 읽었습니다.
BottomSheet가<Modal>보다 위에서useSafeAreaPadding을 호출해서, 모달 창이 아니라 앱을 담고 있는 뷰의 여백을 받습니다.react-native-safe-area-context의 Android 구현은 하단 여백을max(min(view.bottom - windowHeight, 0) + windowInsets.bottom, 0)으로 계산하는데, 이 식은 provider 뷰가 창 하단에서 얼마나 떠 있는지를 빼는 구조입니다. 호스트가 하단 여백을 이미 소비했다면 이 값이0이 됩니다. 실제 로그로도 모달 안에서insets.bottom = 0인데initialWindowMetrics.insets.bottom = 48로 확인했습니다.영향 범위
잘림이 발생하는 곳은
<Modal>에statusBarTranslucent를 전달하는 액션 시트와 사용자 프로필 카드입니다. 리액션 시트 두 개는 이 값을 전달하지 않아 모달 창이 edge-to-edge가 되지 않고 창 decor가 창을 미리 깎아 주므로 잘림은 발생하지 않으며, 여백 값을 앱 트리에서 읽는 문제만 해당합니다.변경 내용
Modal이 앱 창의 모드에 맞춰 모달 창도 edge-to-edge로 맞춥니다. 판정은initialWindowMetrics.frame의 하단을 화면 높이와 비교합니다.frame은android.R.id.content기준이라 창이 시스템 바 뒤까지 그릴 때만 화면 전체가 됩니다. 같은 스냅샷의insets는 decor view 기준이라 edge-to-edge 여부와 무관하게 raw 값을 돌려주므로 판정에 쓸 수 없습니다. 렌더 시점에 계산해 모듈 평가 순서에 의존하지 않게 했습니다.Modal안에SafeAreaProvider를 중첩해서, 모달 내용이 앱 트리가 아니라 자기 창의 여백을 측정합니다.initialMetrics로 초기값을 채워 첫 프레임에 빈 화면이 보이지 않게 했습니다.ModalSafeArea가 모든 방향의 여백을 자식에게 전달합니다. 자식을 함수로 받는 형태인데,ReactionUserListBottomSheet의tabIndex,scrollRef, 탭 인디케이터 애니메이션 값을 모달 안으로 옮기지 않기 위한 선택입니다. RNModal은 닫힐 때 자식을 언마운트하므로 상태를 옮기면 수명이 바뀝니다.테스트 결과
Android · bare RN 0.82 샘플 (Pixel 9 Pro API 36, 3버튼 내비게이션)
0px, 마지막 항목144px가려짐48dp, 항목 4개 모두 노출48dp48dp유지, 이중 여백 없음창 모드 판정이 두 축에서 의도대로 갈리는지 확인했습니다.
frame하단 952 = 화면 952)true48dp, 마지막 항목 하단이 내비게이션 바 상단(2712)에 정확히 맞음. 호스트 소비 여부와 무관하게 동일frame하단 904 < 화면 952)falsergb(0,0,0))축 C의
insets스냅샷은 이 상태에서도bottom: 48을 보고합니다.insets기준으로 판정하면 edge-to-edge가 아닌 앱의 모달까지 내비게이션 바 뒤로 그려지므로,frame기준이어야 합니다.가로 모드로 앱을 시작한 경우(2856×1280, 내비게이션 바 우측), 모달 안에서 읽은 값은 다음과 같습니다. 우측 내비게이션 바에 해당하는
paddingEnd: 48이 들어오고, 가로에서는 하단 내비게이션 바가 없으므로paddingBottom: 0입니다.중앙 정렬 다이얼로그(
Alert)는 프레임을 픽셀 비교했을 때 콘텐츠 영역y 200~2711에서 2512행 중 0행이 달라, 위치가 전혀 이동하지 않았습니다.Android · Expo 56 샘플 (react-native 0.85.3,
react-native-safe-area-context5.7, 제보와 같은 uikit 3.12.6)0px144px가려짐기본 Expo 샘플은
SendbirdUIKitContainer가 트리 최상단에 있어 하단 여백48dp를 정상적으로 받습니다. bare RN에서 얻은 수치와 픽셀 단위로 일치했고, edge-to-edge 강제만으로는 부족하고 호스트의 하단 여백 소비가 함께 필요하다는 점이 두 환경에서 같게 확인됐습니다.iOS (iPhone 16 Pro, iOS 18.2)
1635~2621, 마지막 항목 아이콘 하단2471, 안전 영역 시작2520→102px(34pt) 확보, 가려짐 없음iOS도 모달 창 자신의 여백을 읽으므로 호스트 트리 구성과 무관하게 같은 결과가 나옵니다.
리액션 시트 (Android, 리액션 켠 상태, 호스트가 하단 여백 소비 + 이 PR)
48dp여백 정상ReactionListBottomSheet2295~2711, 마지막 이모지2691→ 가려짐 없음ReactionUserListBottomSheet1727~2711→ 가려짐 없음리액션 시트 두 개는 모달 창이 edge-to-edge가 아니므로 내비게이션 바 바로 위에서 끝나고 별도 여백은 두지 않습니다. 수정 전에도 창이 깎여 있어 여백이 잘려 나갔으므로 화면상 차이는 없습니다. 이 시트들도
48dp를 확보하려면statusBarTranslucent전달을 추가해야 하는데, 티켓 범위를 넘는 시각 변경이라 이 PR에서는 다루지 않았습니다.기존 사용자에게 미치는 영향
navigationBarTranslucent와 edge-to-edge 판정이 Android로 제한되어 있어 창 모드 변화가 없습니다. 모달 안에서 읽는 여백 값만 자기 창 기준으로 바뀝니다.false가 되어 모달 창 모드가 바뀌지 않습니다. 내비게이션 바도 기존처럼 불투명하게 유지됩니다(축 C에서 측정).styles.statusBarTranslucent를false로 둔 앱도 기존 경로를 그대로 사용합니다.edgeToEdgeEnabled를 켜지 않았지만 targetSdk 35·36으로 Android 15·16에서 동작하는 앱도 OS가 액티비티를 edge-to-edge로 만들기 때문에 이 변화를 보게 됩니다.false가 되어 여백을 확보하지 않습니다. 이때는 모달 창도 내비게이션 바 아래로 확장되지 않으므로 잘림 없이 여백만 없는 상태가 됩니다.ModalSafeAreaexport 추가뿐이고,BottomSheet의 props는 그대로입니다. 네이티브 코드와 의존성 변경은 없습니다.확인한 검사
yarn lint,yarn build(5개 패키지),tsc, jest 34 suites / 190 tests 모두 통과했습니다.yarn test를 그대로 실행하면 50개 suite가 실패하는데, 이 PR과 무관하게 로컬의.claude/worktrees/사본이 jest haste map에서 패키지 이름을 중복 제공해서 생기는 문제입니다.--modulePathIgnorePatterns "/.claude/worktrees/"를 주면 모두 통과합니다.CHANGELOG는 릴리스 시점에 추가하는 것으로 두고 이 PR에서는 건드리지 않았습니다.
Types Of Changes
What types of changes does your code introduce to this project?
Put an
xin the boxes that apply_🤖 Generated with Claude Code