release: 1.7.2 - #46
Merged
Merged
Conversation
푸시 알림 탭과 유니버설 링크로 진입하는 상세 화면은 홈 웹셸이 아니라 ClubDetailScreen의 WebView에서 돈다. 이 컨테이너가 쓰는 use-webview-message-handler는 옛 NOTIFICATION_SUBSCRIBE/UNSUBSCRIBE만 알고, 웹이 6월(#1665)부터 보내는 SUBSCRIBE_TOGGLE을 default 분기에서 버렸다. 그래서 종을 눌러도 앱이 아무 것도 하지 않았고, 회신이 없어 웹 토스트도 뜨지 않았다. 홈 카드로 들어온 경우는 같은 웹 화면이 홈 웹셸 WebView 안에서 SPA 라우팅으로 열려 정상 동작했다. - SUBSCRIBE_TOGGLE / REQUEST_SUBSCRIBE_STATE 처리와 회신 추가 - 회신은 홈 웹셸과 동일하게 injectJavaScript로 window에 MessageEvent를 dispatch한다. 웹은 window의 message 이벤트만 듣는다 - clubId는 라우트 파라미터가 아니라 페이로드 값을 쓴다. 라우트 id는 @동아리명 슬러그일 수 있고 페이로드는 항상 ObjectId다 - 권한 거부 시 subscribed에 변경 전 값을 그대로 실어 웹이 "구독 완료"로 오인하지 않게 한다 - OPEN_APP_SETTINGS를 받아 Linking.openSettings()를 호출한다. 권한 안내 토스트 탭으로 설정 앱을 여는 웹 변경(#2024)의 앱 쪽 짝이다 옛 NOTIFICATION_SUBSCRIBE/UNSUBSCRIBE는 구버전 웹 호환용으로 유지한다.
appendSessionId는 sessionId가 비면 URL을 그대로 돌려준다. 그 뒤 구독 여부를 붙이는 쪽이 앞 단계에서 ?가 생겼다고 가정하고 &를 쓰는데, ?가 없으면 브라우저는 & 이후를 쿼리로 승격시키지 않고 경로의 일부로 읽는다. 그러면 라우트 /webview/club/:clubId 의 clubId가 "<id>&is_subscribed=true" 가 되어 동아리 조회에 실패하고 상세 화면이 뜨지 않는다. appendSessionId가 쓰는 것과 같은 분기로 구분자를 고른다. sessionId가 비어도 clubId는 온전하고 session_id만 빠진다. sessionId가 비는 순간(부트스트랩 완료 전 딥링크 진입)은 코드상 도달 가능하다고 판단했을 뿐 재현하지는 못했다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
홈이 웹뷰로 전환되면서(feature/webview-shell-migration) 라우팅이 바뀌었는데 문서가 따라가지 않았다. 코드로 확인한 불일치만 고친다. - (tabs)/ 하단 탭 네비게이터는 존재하지 않는다. 홈은 app/index.tsx이고 HomeWebViewScreen 우선, 로드 실패 시 네이티브 home-screen 폴백이다. - clubDetail/[id].tsx는 "네이티브 상세"가 아니라 club/[id].tsx와 동일한 ClubDetailScreen 재export다. FCM 딥링크가 이 경로로 들어온다. - 부트스트랩은 토큰 발급만 순차고, 구독 동기화·Mixpanel identify·FCM 등록은 Promise.all 병렬이라 순서가 없다. ATT는 2번째가 아니라 부트스트랩 성공 후 스플래시가 내려간 뒤에 요청한다. - moa-text가 export하는 이름은 Text가 아니라 MoaText다. - HomeWebViewPreloadProvider가 빠져 있었다. 스플래시 종료 시점을 결정한다. - 환경 변수에 EXPO_PUBLIC_WEBVIEW_URL, EXPO_PUBLIC_MIXPANEL_TOKEN 추가. - ui/ 하위는 현재 home과 club-detail 둘이고 hook/·model/은 home에만 있다. 개요 문단도 고쳤다. 동아리 상세만 웹뷰인 것처럼 읽혔는데, 홈까지 웹뷰이고 네이티브가 셸이라는 게 이 레포를 읽을 때 가장 먼저 알아야 할 구조다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
uri useMemo가 isSubscribed에 의존하고 있어서, 구독을 토글하면 subscribed-clubs-context가 setSubscribedClubIds로 목록을 갈아끼우고 → isSubscribed 함수 아이덴티티가 바뀌고 → uri가 재계산되어 is_subscribed=true가 붙거나 빠진다. 그러면 source.uri가 바뀌고 웹뷰가 그 URL을 다시 로드한다. react-native-webview 13.15.0 기준: - iOS: RNCWebViewImpl.m setSource가 source 딕셔너리를 비교해 다르면 visitSource -> loadRequest. - Android: RNCWebViewManagerImpl.kt loadSource가 view.url(현재 표시 중인 URL)과 비교하는데, 상세는 /webview/club/:id 로 진입해 /clubDetail/:id 로 리다이렉트되고 웹이 history.replaceState로 session_id까지 지우므로 두 값이 같아질 수 없다. 즉 가드가 무력해서 source prop이 바뀌면 항상 loadUrl. 지금은 이 화면에서 구독을 토글할 방법이 없어 드러나지 않는다. 웹 종 버튼이 보내는 SUBSCRIBE_TOGGLE을 use-webview-message-handler가 모르고, 네이티브 종 버튼은 hasError일 때만 렌더링된다. #34가 SUBSCRIBE_TOGGLE 처리를 추가하면 종을 누를 때마다 리로드가 발생한다. 웹은 이 파라미터를 ClubDetailPage의 initialIsSubscribed(첫 페인트 깜빡임 방지) 로만 쓰고 이후 상태는 SUBSCRIBE_STATE 메시지로 받으므로, 마운트 시점 값으로 고정해도 기능이 줄지 않는다. 파라미터를 없애면 첫 페인트가 깜빡여서 남긴다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
iOS의 expo-notifications는 원격 푸시일 때 content.data를 userInfo["body"]에서만 꺼낸다(EXNotificationSerializer.m serializedNotificationData). 그건 Expo 푸시 서비스 포맷이고, FCM은 커스텀 키를 userInfo 최상위에 둔다. 그래서 userInfo["body"]가 nil이 되고 content.data가 null로 내려온다. 그러면 콜드 스타트 경로의 가드가 항상 거짓이라 handleNotificationData가 한 번도 불리지 않고, 앱은 열리되 홈에 머문다. Android는 같은 상황을 명시적으로 분기해 FCM data를 content.data로 그대로 복사한다 (NotificationSerializer.java). 그래서 Android만 정상이었다. iOS도 원본을 버리지는 않는다. 같은 직렬화가 trigger.payload에 userInfo를 통째로 남긴다(EXNotificationSerializer.m serializedNotificationTrigger). 타입 문서에도 명시돼 있다. 그래서 네이티브 수정이나 @react-native-firebase/messaging의 별도 탭 핸들러 없이, content.data가 비었을 때만 trigger.payload로 폴백하면 된다. Android 경로를 바꾸지 않도록 content.data를 항상 먼저 본다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
우체통은 로그인 없이 쓰므로 익명 학생 토큰으로 사람을 구분한다. 웹은 앱이 주입한 토큰을 먼저 쓰고(studentFetch), 없으면 자기 토큰을 발급한다. 그러면 앱과 신원이 갈려서 백엔드가 Feedback.studentId로 찾는 편지함이 비어 보인다. 주입이 홈 웹뷰에만 있었다. 답장 푸시는 path=/feedback/letters/<id> 를 보내고 (FeedbackAdminService), use-fcm 은 /webview/clubDetail/ 로 시작하지 않는 path 를 /webview/[slug] 로 넘긴다. 그 화면에는 주입이 없어서, 답장 알림을 탭해 연 편지함이 방금 보낸 편지를 못 찾는다. 주입 스크립트 생성을 utils/webview.ts 로 올려 홈과 공유한다. 가드 기준을 진입 URL이 아니라 모아동 URL로 바꿨다. 기존 홈 코드는 new URL(BASE_URL).origin !== window.location.origin 으로 비교했는데, 홈은 BASE_URL 이 항상 모아동이라 맞지만 webview/[slug] 는 진입 URL 자체가 외부일 수 있어(slug=external) "외부 == 외부" 로 통과해 버린다. 그대로 옮기면 외부 사이트에 베어러 토큰이 주입된다. 그래서 가드가 두 겹이다. RN 쪽에서 진입 URL이 모아동 오리진일 때만 스크립트를 만들고, 스크립트 안에서 실행 시점 origin 을 다시 본다(웹뷰가 나중에 외부로 이동하는 경우). 두 번째 비교는 웹뷰 안에서 한다 - RN 의 URL 폴리필이 호스트 대소문자와 기본 포트를 정규화하지 않아 RN 에서 만든 origin 문자열이 window.location.origin 과 어긋날 수 있다. 주입은 content load 이전에 끝나야 하므로 토큰이 정해질 때까지 웹뷰를 렌더하지 않는다. 외부 URL 진입이면 주입할 일이 없으니 기다리지 않는다. club-detail-screen 에는 넣지 않았다. studentFetch 를 쓰는 페이지를 직접 열지 않는다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
iOS의 trigger.payload는 FCM data뿐 아니라 aps 등 userInfo 전체라, Android의 content.data(FCM data만)보다 표면이 넓다. 그런데 handleNotificationData는 as string 단언만 해서, path가 문자열이 아니면 targetPath.startsWith 에서 던진다. 응답 리스너 경로에는 catch가 없어 그대로 올라간다. 라우팅에 쓰는 action/clubId/path만 typeof로 확인한다. 아닌 값은 버려서 이동하지 않는다. CodeRabbit 지적 반영. payload가 객체인지까지는 확인하지 않는다. trigger.payload 타입이 Record이고 iOS 직렬화가 userInfo 딕셔너리를 그대로 넣으므로 일어나지 않는 상황이다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
두 증상이 한 경로에서 나온다. 홈 웹뷰에서 외부 링크를 탭하면 handleShouldStartLoadWithRequest 가 가로채 /webview/[slug] 로 넘기는데, 그 화면이 목적지를 가리지 않고 appendSessionId 를 붙였다. iOS: session_id 는 웹 Mixpanel 의 distinct_id 다. 그게 쿼리에 실려 제3자 도메인으로 나가 상대 액세스 로그에 남는다. 모아동 오리진일 때만 붙인다. Android: setSupportMultipleWindows 기본값이 true 라 target=_blank 가 onCreateWindow 로 간다. onOpenWindow 핸들러가 없으면 RNCWebChromeClient 가 WebViewClient 도 없는 new WebView(context) 를 만들어 transport 로 넘기는데, 그 뷰는 어떤 계층에도 붙지 않는다. 그래서 링크를 눌러도 아무 일이 안 일어난다. false 로 두면 같은 요청이 onShouldStartLoadWithRequest 를 타 iOS 와 같은 경로가 된다. 둘을 같이 고친다. Android 링크만 살리면 session_id 가 나가는 경로가 Android 로도 번진다. 대상은 preventDefault 없는 생 <a target="_blank"> 들이다(ClubUnionPage 의 인스타·카톡, IntroducePage 의 문의하기). useNavigator 를 거치는 링크는 requestOpenExternalUrl -> WebBrowser 로 나가므로 원래 영향이 없다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
request.url.startsWith(baseOrigin) 은 https://moadong.com.evil.com 을 내부 URL 로 승인한다. 그 페이지가 홈 웹뷰에 뜨면 window.ReactNativeWebView.postMessage 로 브리지를 그대로 쓸 수 있다. 이 웹뷰는 onMessage 에서 SUBSCRIBE_TOGGLE, NAVIGATE_WEBVIEW, OPEN_EXTERNAL_URL, SHARE 를 origin 검증 없이 처리한다. 파싱한 origin 끼리 비교한다. #40 에서 추가한 isWebViewOrigin 을 그대로 쓴다. 주입 토큰 쪽은 원래 new URL(...).origin 으로 비교하고 있어 영향이 없었다. setSupportMultipleWindows={false} 로 Android 의 target=_blank 요청도 이 검사에 들어오므로 같이 고친다. CodeRabbit 지적 반영. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
외부 사이트를 앱 화면(WebView)에 띄우고 있었다. 사용자에게는 모아동 헤더가 붙은 채로 뜨고 제목은 기본값 "웹페이지" 라 어느 사이트인지 알 단서가 없다. 주소창도 없다. 기술적으로 더 문제인 건 그 WebView 가 앱 프로세스 안이라는 것이다. onMessage 가 붙어 있어 외부 페이지가 window.ReactNativeWebView.postMessage 로 SUBSCRIBE_TOGGLE, NAVIGATE_WEBVIEW, OPEN_EXTERNAL_URL 을 그대로 호출할 수 있다. WebBrowser.openBrowserAsync 로 넘긴다. iOS SFSafariViewController, Android Chrome Custom Tabs 라 OS 브라우저 프로세스에서 돌고 앱 JS 에 닿지 못한다. 도메인이 표시되고 시스템 브라우저 세션을 공유한다. 새로 도입하는 방식이 아니다. 배너(banner.tsx), 동아리 SNS(useNavigator -> OPEN_EXTERNAL_URL), external-link.tsx 가 이미 같은 함수를 쓴다. slug=external 경로만 예외로 남아 있었다. 열기 실패는 로그만 남긴다. 실패해도 웹뷰 이동은 이미 취소된 상태라 조용히 아무 일도 안 일어나면 원인을 알 수 없다. router 는 이 콜백에서 더 쓰이지 않아 deps 에서 뺀다. handleMessage 의 내부 라우팅에는 그대로 쓴다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
앞 커밋으로 홈의 외부 링크가 OS 브라우저로 가면서, 앱 안에서 이 화면에 url 파라미터를 넘기는 곳이 없어졌다. /webview/[slug] 로 가는 호출 4곳은 모두 path 나 slug 만 넘긴다. 그런데 파라미터를 받는 쪽이 남아 있으면 moadongapp://webview/x?url=https://... 딥링크로 임의 사이트를 이 화면에 띄울 수 있다. 이 화면은 onMessage 가 붙어 있어서 그 페이지가 window.ReactNativeWebView.postMessage 로 앱 브리지를 그대로 쓴다. 사용자에게는 모아동 헤더가 붙은 화면으로 보인다. 목적지를 path 또는 pageConfig 로만 정한다. 등록되지 않은 slug 는 기존 오류 화면으로 간다. 오리진 가드는 그대로 둔다. pageConfig 에 외부 URL 항목(privacy-policy 의 notion 주소)이 남아 있어 baseUrl 이 모아동이 아닐 수 있다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
한 사람이 Mixpanel에 두 명으로 기록되고 있다. 네이티브는 user:<JWT sub>, 웹뷰 안의 웹은 moadong_<ts>_<rand> 로 identify 하고 둘을 잇는 머지가 없다. f496e47 이전에는 네이티브도 session_id 로 identify 해서 한 사람이었다. 대안 셋을 적고 B(웹을 user:<sub> 로 옮김)로 확정한다. 근거는 신원의 복구 가능성이다. session_id 는 AsyncStorage 에만 있고 스토리지 오류 시 저장 없이 일회용 ID 를 반환하지만(utils/mixpanel.ts:27-30), sub 는 서버 StudentUser.studentId 에 있어 재발급으로 같은 신원을 복구할 수 있다. 앱과 웹이 같은 Mixpanel 프로젝트를 쓰는 것은 데이터로 확인했다. 프로젝트 3611536 에 네이티브 전용 속성(url=app://moadong)을 가진 이벤트와 웹 이벤트가 함께 있다. 토큰 문자열은 레포에서 볼 수 없지만 이벤트가 같이 쌓이는 것을 확인한 쪽이 더 강한 증거다. 그래서 선행 조건은 없고 바로 진행할 수 있다. 로그인 도입이 예정돼 있어 이 결정이 선행 조건이다. 익명 신원이 통일돼 있으면 로그인 시 identify 한 번으로 전체가 한 사람이 되지만, 갈라져 있으면 한쪽 이력이 고아로 남는다. ID Merge 모드(Original/Simplified)는 이 작업이 아니라 로그인 작업의 입력값이라는 것도 적었다. 되돌릴 조건을 명시했다. 코드는 한 줄 되돌리면 되지만 Mixpanel 클러스터 병합은 취소할 수 없다. 그래서 되돌릴 판단은 코드 롤백이 아니라 신원 축 유지 여부 수준에서 한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
initialReady 가 optional 이라 "안 넘기는 경우"를 위한 폴백 분기가 있었고, 그
분기가 프로바이더 안에서 세션 ID 를 만들고 identify 까지 했다
(getMixpanelDistinctId -> identifyMixpanel).
마운트 지점은 app/_layout.tsx 한 곳뿐이고 initialReady={bootstrapSucceeded} 를
항상 넘긴다. bootstrapSucceeded 는 bootstrapStatus === 'success' 로 늘 boolean
이라 usesBootstrapState 가 항상 참이고, effect 는 항상 early return 한다.
즉 그 분기는 도달하지 않는다.
문제는 죽은 채로 남아 신원 결정 로직이 두 곳에 있는 것처럼 읽힌다는 것이다.
이번 신원 조사에서 실제로 이걸 방어 코드로 착각했다. 신원은 부트스트랩에서만
정한다(app-bootstrap.service.ts).
initialReady 를 필수 prop 으로 바꿔 분기가 다시 생기지 않게 컴파일러로 막는다.
props 를 state 에 복사하던 useState + useEffect 도 없앤다. 값이 props 에서만
나오므로 직접 파생하면 되고, prop 이 바뀐 직후 한 렌더 동안 이전 값이 보이던
것도 사라진다.
identifyMixpanel 과 getOrCreateMixpanelSessionId 는 부트스트랩이 계속 쓰므로
남긴다. 이 파일의 import 만 정리한다.
관련 결정: docs/MIXPANEL_IDENTITY_DECISION.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
초안은 "이 결정이 로그인의 선행 조건"이라고 적었다. 로그인 화면이 네이티브라는 가정이었고, 그 가정이 틀렸다. 로그인은 전부 웹뷰 안에서 이뤄진다. 그러면 로그인 전 탐색과 로그인 이벤트가 모두 웹 SDK 한쪽에서 일어나므로 identify(accountId) 가 그 신원을 그대로 병합한다. 가입 퍼널이 깨지지 않는다. 신원이 통일돼 있든 아니든 마찬가지다. 남는 손실은 네이티브 이벤트(permission-dialog, banner, club-detail-screen, 폴백 home-screen)가 계정에 붙지 않는 것뿐이고 그 규모가 90일 유니크 2명이다. 웹 레포 PR 과 비가역 클러스터 병합 리스크를 감당할 이득이 아니다. 방향은 B 로 유지하고 재개 조건 네 개를 적는다. 네이티브 비중 증가, 네이티브 이벤트의 계정 귀속 요구, 로그인의 네이티브 전환, 실제 분석 오류 사례. 로그인 작업에서 따로 필요한 것을 기록했다. 로그인이 웹뷰 안이면 앱 네이티브는 로그인 상태와 계정 ID 를 모른다. 네이티브 이벤트를 계정에 귀속시키거나 계정 단위 푸시를 보내려면 웹이 로그인 결과를 브리지로 알려주는 메시지가 필요한데 지금 브리지에 없다. 이 문서의 결정과는 독립이다. 앞 커밋의 죽은 코드 제거는 유지한다. 결정과 무관하게 옳은 정리다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs: CLAUDE.md를 현재 코드 구조에 맞춘다
docs+refactor: Mixpanel 신원 통일 검토(보류) / 도달 불가 폴백 제거
fix: iOS에서 푸시 탭이 화면 이동을 못 하던 것을 고친다
…view fix: 푸시로 열리는 웹뷰의 학생 토큰 주입과 외부 링크 처리를 고친다
fix: 동아리 상세 웹뷰 URL 조립을 고친다 (clubId 오염 + 토글 시 리로드)
fix: 상세 웹뷰 컨테이너가 SUBSCRIBE_TOGGLE을 처리하도록 한다
같은 웹 화면이 홈 웹셸, ClubDetailScreen, webview/[slug] 세 WebView 안에서 도는데 메시지 처리 구현은 두 벌이다. 공용 훅과 홈의 인라인 switch가 서로를 모른 채 각자 자라서, 호스트마다 아는 메시지 목록이 다르다. 그 자체보다 나쁜 건 어긋남이 조용하다는 점이다. 공용 훅의 default는 console.warn만 남기고 홈의 switch는 default조차 없이 바깥 catch가 삼킨다. 그래서 웹이 6월(#1665)부터 보내던 SUBSCRIBE_TOGGLE을 상세 컨테이너가 버리고 있다는 사실을 12월에 사용자 제보로 알았다(ffb53d0). - 각 호스트 switch의 default에서 reportUnknownBridgeMessage를 부른다. dev는 console.warn, prod는 Mixpanel Bridge UnknownMessage 이벤트 - 판별 기준을 "알려진 타입 집합과 대조"가 아니라 "이 호스트의 switch가 잡지 못함"으로 뒀다. 홈만 처리하는 REQUEST_APP_VERSION처럼 호스트별로 목록이 다른 지금 구조에서, 집합 대조는 정상 처리된 메시지를 오탐한다 - 서드파티가 쏜 비정형 메시지를 거르려고 문자열 type만 취급한다. JSON 파싱 실패는 대상이 아니다. 기존 로그로 이미 드러나고, 비JSON을 쏘는 스크립트가 있으면 노이즈가 된다 - useWebViewMessageHandler의 host를 필수 옵션으로 뒀다. 웹뷰 호스트가 늘어날 때 타입 에러로 막혀 이름을 붙이게 된다 리포터는 void를 반환하고 예외를 밖으로 내보내지 않는다. 관측이 실패해도 메시지 처리는 계속돼야 한다. 기존 9종의 처리 경로는 건드리지 않았다. 갈라진 구현을 한 벌로 합치는 일은 이 PR의 범위가 아니다. 홈에만 있는 REQUEST_APP_VERSION, 호스트마다 다른 OPEN_EXTERNAL_URL 구현, NAVIGATE_BACK의 서로 다른 처리를 훅으로 흡수해야 해서 규모가 다르다. 그 전까지 이 관측 장치가 어긋남을 드러낸다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rting-v2 chore: 호스트가 처리하지 못한 웹뷰 메시지를 관측한다
앱 버전 1.7.2, iOS buildNumber 19, Android versionCode 19. 버그 수정 릴리즈라 patch 를 올린다. 외부 링크가 앱 화면 대신 OS 브라우저로 열리는 동작 변화가 있지만 기능 추가는 아니다. 지난 bump 커밋(8135024)과 같은 3개 파일을 고친다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
chore: bump app version to 1.7.2
|
Warning Review limit reachedNext included review available in 35 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (14)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
릴리즈 개요
1.7.21919prod이전 릴리즈
1.7.1(#33) 이후 누적된 버그 수정 릴리즈입니다. 기능 추가는 없습니다.주요 변경사항
푸시 알림
data를userInfo["body"](Expo 푸시 서비스 포맷)에서만 꺼내는데 FCM은 커스텀 키를 userInfo 최상위에 둡니다. 그래서content.data가null이 되어 딥링크 핸들러가 한 번도 호출되지 않았습니다. Android는 같은 상황을 분기 처리하고 있어 정상이었습니다. iOS도trigger.payload에 userInfo 원본이 남아 있어 그쪽으로 폴백합니다 (#39)action/clubId/path)이 문자열일 때만 사용합니다. iOStrigger.payload는aps등 userInfo 전체라 표면이 넓고, 단언만 하면 비문자열path에서 예외가 응답 리스너 밖으로 올라갔습니다 (#39)우체통 신원
path=/feedback/letters/<id>)로 열리는webview/[slug]화면에서는 웹이 자체 토큰을 발급해 앱과 신원이 갈렸습니다 (#40)외부 링크
WebView)에 띄우던 것을 OS 브라우저(SFSafariViewController/ Chrome Custom Tabs)로 바꿉니다. 기존에는 모아동 헤더가 붙고 제목이 기본값 "웹페이지"로 떠서 어느 사이트인지 알 단서가 없었고, 앱 프로세스 안이라 그 페이지가window.ReactNativeWebView로 브리지를 쓸 수 있었습니다. 배너·동아리 SNS·OPEN_EXTERNAL_URL이 이미 쓰던 방식으로 통일합니다 (#40)setSupportMultipleWindows기본값 때문에target="_blank"가 화면에 붙지 않는 WebView로 빨려 들어갔습니다 (#40)session_id를 붙이지 않습니다. 모아동 오리진일 때만 부착합니다 (#40)https://moadong.com.evil.com이 내부로 승인되던 문제입니다 (CWE-346, #40)url쿼리 파라미터로 받지 않습니다.moadongapp://webview/x?url=...딥링크로 임의 사이트를 앱 화면에 띄울 수 있었습니다 (#40)동아리 상세 웹뷰
&를 붙여clubId가 오염되던 것을 고칩니다.?가 없는데&를 붙이면 그 뒤가 경로로 읽혀clubId가<id>&is_subscribed=true가 되고 동아리 조회에 실패했습니다 (#36)uri를 마운트 시점 구독 상태로 고정합니다. 실시간 상태는SUBSCRIBE_STATE메시지로 전달됩니다 (#36)SUBSCRIBE_TOGGLE을 처리합니다. 푸시·유니버설 링크로 들어온 상세에서 종 버튼이 무반응이었습니다.OPEN_APP_SETTINGS도 추가합니다 (#34)관측
문서 / 정리
CLAUDE.md를 현재 코드 구조에 맞춥니다 (#37)MixpanelProvider폴백 분기를 제거합니다 (#43)검증
main기준npx tsc --noEmit통과 (exit 0)main기준npm run lint통과 — 오류 0건, 기존 경고 5건(import/no-named-as-default)Android Check통과npx expo config --type public --json에서1.7.2/ iOS19/ Android19확인plutil -lint ios/app/Info.plist통과1.7.1) 잔여 0건이 릴리즈의 변경은 실기기에서 검증되지 않았습니다. 타입체크·린트·로직 시뮬레이션만 통과한 상태입니다. 아래는 코드로 확인할 수 없는 항목입니다.
data에action/path누락 (앱 수정 아님)session_id없음6번은 사용자에게 바로 보이는 동작 변화입니다 — 외부 링크가 모아동 화면이 아니라 브라우저 시트로 열립니다.
별건으로 확인해둘 것
app.json의ios.entitlements.aps-environment가development입니다. 배포 빌드는production이어야 합니다. 현재 알림이 도착하고 있으니 빌드 과정에서 덮어쓰이는 것으로 보이지만 확인해두는 편이 좋습니다.