From ddddf1ce66c9ed6f194880f88b2ae81af2ac9623 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Tue, 22 Sep 2026 15:23:43 +0900 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=ED=91=B8=EC=8B=9C=EB=A1=9C=20?= =?UTF-8?q?=EC=97=B4=EB=A6=AC=EB=8A=94=20=EC=9B=B9=EB=B7=B0=EC=97=90?= =?UTF-8?q?=EB=8F=84=20=ED=95=99=EC=83=9D=20=ED=86=A0=ED=81=B0=EC=9D=84=20?= =?UTF-8?q?=EC=A3=BC=EC=9E=85=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 우체통은 로그인 없이 쓰므로 익명 학생 토큰으로 사람을 구분한다. 웹은 앱이 주입한 토큰을 먼저 쓰고(studentFetch), 없으면 자기 토큰을 발급한다. 그러면 앱과 신원이 갈려서 백엔드가 Feedback.studentId로 찾는 편지함이 비어 보인다. 주입이 홈 웹뷰에만 있었다. 답장 푸시는 path=/feedback/letters/ 를 보내고 (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) --- app/webview/[slug].tsx | 90 ++++++++++++++++++++++++--------- ui/home/home-webview-screen.tsx | 17 +------ utils/webview.ts | 41 +++++++++++++++ 3 files changed, 108 insertions(+), 40 deletions(-) diff --git a/app/webview/[slug].tsx b/app/webview/[slug].tsx index 6cf9104..a5c4e70 100644 --- a/app/webview/[slug].tsx +++ b/app/webview/[slug].tsx @@ -6,10 +6,11 @@ import { MoaText } from "@/components/moa-text"; import { useMixpanelContext } from "@/contexts/mixpanel-context"; import { useWebViewMessageHandler } from "@/hooks/use-webview-message-handler"; -import { appendSessionId, getWebViewUserAgent } from "@/utils/webview"; +import { ensureAccessToken } from "@/services/auth-token.service"; +import { appendSessionId, buildStudentTokenInjection, getWebViewUserAgent, isWebViewOrigin } from "@/utils/webview"; import { Ionicons } from "@expo/vector-icons"; import { useLocalSearchParams, useRouter } from "expo-router"; -import React, { useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { ActivityIndicator, TouchableOpacity } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { WebView } from "react-native-webview"; @@ -77,6 +78,42 @@ export default function WebViewScreen() { const url = useMemo(() => appendSessionId(baseUrl, sessionId), [baseUrl, sessionId]); + /** + * 우체통은 앱이 주입한 학생 토큰을 먼저 쓴다(웹 studentFetch). 주입이 없으면 웹이 + * 자체 토큰을 발급해 앱과 신원이 갈리고, 답장 푸시로 열린 편지함이 비어 보인다. + * 답장 푸시의 path가 /feedback/letters/... 라 이 화면으로 들어온다. + * + * 주입은 content load 이전에 끝나야 하므로 토큰이 정해질 때까지 웹뷰를 렌더하지 않는다. + * 외부 URL로 진입한 경우에는 주입할 일이 없으니 기다리지도 않는다. + */ + const [studentToken, setStudentToken] = useState(null); + const [tokenResolved, setTokenResolved] = useState(false); + + useEffect(() => { + if (!isWebViewOrigin(baseUrl)) { + setTokenResolved(true); + return; + } + + let cancelled = false; + ensureAccessToken() + .then((token) => { + if (!cancelled) setStudentToken(token); + }) + .catch(() => { + if (!cancelled) setStudentToken(null); + }) + .finally(() => { + if (!cancelled) setTokenResolved(true); + }); + + return () => { + cancelled = true; + }; + }, [baseUrl]); + + const injectedToken = buildStudentTokenInjection(baseUrl, studentToken); + const userAgent = getWebViewUserAgent(); const handleBack = () => { @@ -150,29 +187,32 @@ export default function WebViewScreen() { )} - { - if (!hasLoadedOnce) { - setLoading(true); - setError(false); - } - }} - onLoadEnd={() => { - setLoading(false); - if (!hasLoadedOnce) { - setHasLoadedOnce(true); - } - }} - onError={() => { - setError(true); - setLoading(false); - }} - javaScriptEnabled={true} - domStorageEnabled={true} - /> + {tokenResolved && ( + { + if (!hasLoadedOnce) { + setLoading(true); + setError(false); + } + }} + onLoadEnd={() => { + setLoading(false); + if (!hasLoadedOnce) { + setHasLoadedOnce(true); + } + }} + onError={() => { + setError(true); + setLoading(false); + }} + javaScriptEnabled={true} + domStorageEnabled={true} + /> + )} diff --git a/ui/home/home-webview-screen.tsx b/ui/home/home-webview-screen.tsx index be1bac7..5c79613 100644 --- a/ui/home/home-webview-screen.tsx +++ b/ui/home/home-webview-screen.tsx @@ -2,7 +2,7 @@ import { useHomeWebViewPreloadContext } from '@/contexts/home-webview-preload-co import { useMixpanelContext } from '@/contexts/mixpanel-context'; import { useSubscribedClubsContext } from '@/contexts/subscribed-clubs-context'; import { ensureAccessToken } from '@/services/auth-token.service'; -import { appendSessionId, getWebViewUserAgent } from '@/utils/webview'; +import { appendSessionId, buildStudentTokenInjection, getWebViewUserAgent } from '@/utils/webview'; import Constants from 'expo-constants'; import { useRouter } from 'expo-router'; import * as WebBrowser from 'expo-web-browser'; @@ -61,20 +61,7 @@ export function HomeWebViewScreen({ onError }: HomeWebViewScreenProps) { const url = sessionLoading || !tokenResolved ? null : appendSessionId(BASE_URL, sessionId); - // 주입 스크립트는 웹뷰가 로드하는 모든 문서에서 실행되므로, - // origin 가드 없이는 외부 사이트로 이동했을 때 베어러 토큰이 노출된다. - // origin 비교는 웹뷰 안에서 한다. RN 의 URL 폴리필은 호스트 대소문자와 기본 포트를 - // 정규화하지 않아 window.location.origin 과 어긋날 수 있다. 파싱에 실패하면 주입하지 않는다. - const injectedToken = studentToken - ? `(function(){ - try { - if (new URL(${JSON.stringify(BASE_URL)}).origin !== window.location.origin) return; - } catch (e) { - return; - } - window.__MOADONG_STUDENT_TOKEN__ = ${JSON.stringify(studentToken)}; - })(); true;` - : undefined; + const injectedToken = buildStudentTokenInjection(BASE_URL, studentToken); useEffect(() => { if (url) { diff --git a/utils/webview.ts b/utils/webview.ts index 7d6f847..35cb69b 100644 --- a/utils/webview.ts +++ b/utils/webview.ts @@ -12,3 +12,44 @@ export const appendSessionId = (url: string, sessionId: string): string => { const separator = url.includes('?') ? '&' : '?'; return `${url}${separator}session_id=${encodeURIComponent(sessionId)}`; }; + +const WEBVIEW_URL = process.env.EXPO_PUBLIC_WEBVIEW_URL || 'https://moadong.com'; + +/** 웹뷰가 로드할 URL이 모아동 오리진인지. 학생 토큰을 주입해도 되는지의 기준이다. */ +export const isWebViewOrigin = (url: string): boolean => { + try { + return new URL(url).origin === new URL(WEBVIEW_URL).origin; + } catch { + return false; + } +}; + +/** + * 웹이 첫 API 호출 시점에 앱과 같은 학생 신원을 쓰도록 토큰을 주입하는 스크립트. + * + * 가드가 두 겹이다. + * 1. 진입 URL이 외부일 수 있어(webview/[slug]의 external) RN 쪽에서 먼저 막는다. + * 비교 기준은 진입 URL이 아니라 모아동 URL이어야 한다 - 진입 URL끼리 비교하면 + * 외부 URL로 진입했을 때 "외부 == 외부"로 통과해 버린다. + * 2. 주입 스크립트는 웹뷰가 로드하는 모든 문서에서 실행되므로 스크립트 안에서도 + * 실행 시점 origin을 다시 본다. 없으면 외부 사이트로 이동했을 때 토큰이 노출된다. + * + * 2번의 origin 비교는 웹뷰 안에서 한다. RN의 URL 폴리필은 호스트 대소문자와 기본 포트를 + * 정규화하지 않아, RN에서 만든 origin 문자열이 window.location.origin과 어긋날 수 있다. + * 그래서 모아동 URL을 그대로 넘겨 웹뷰의 URL로 파싱한다. 파싱에 실패하면 주입하지 않는다. + */ +export const buildStudentTokenInjection = ( + targetUrl: string, + token: string | null, +): string | undefined => { + if (!token || !isWebViewOrigin(targetUrl)) return undefined; + + return `(function(){ + try { + if (new URL(${JSON.stringify(WEBVIEW_URL)}).origin !== window.location.origin) return; + } catch (e) { + return; + } + window.__MOADONG_STUDENT_TOKEN__ = ${JSON.stringify(token)}; + })(); true;`; +}; From e537a3485466645a25149b5561747e814471af09 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Tue, 22 Sep 2026 16:43:52 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=EC=99=B8=EB=B6=80=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=EC=97=90=20session=5Fid=EB=A5=BC=20=EB=B6=99=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EA=B3=A0=20Android=EC=97=90=EC=84=9C?= =?UTF-8?q?=EB=8F=84=20=EC=97=B4=EB=A6=AC=EA=B2=8C=20=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 두 증상이 한 경로에서 나온다. 홈 웹뷰에서 외부 링크를 탭하면 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 없는 생 들이다(ClubUnionPage 의 인스타·카톡, IntroducePage 의 문의하기). useNavigator 를 거치는 링크는 requestOpenExternalUrl -> WebBrowser 로 나가므로 원래 영향이 없다. Co-Authored-By: Claude Opus 5 (1M context) --- app/webview/[slug].tsx | 9 ++++++++- ui/home/home-webview-screen.tsx | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/webview/[slug].tsx b/app/webview/[slug].tsx index a5c4e70..d7ad962 100644 --- a/app/webview/[slug].tsx +++ b/app/webview/[slug].tsx @@ -76,7 +76,14 @@ export default function WebViewScreen() { ? (config.url ?? (config.path ? `${webviewUrl}${config.path}` : "")) : ""; - const url = useMemo(() => appendSessionId(baseUrl, sessionId), [baseUrl, sessionId]); + /** + * session_id 는 웹 Mixpanel 의 distinct_id 라 모아동 밖으로 나가면 안 된다. + * 이 화면은 slug=external 로 임의 외부 URL 도 로드하므로 오리진을 확인하고 붙인다. + */ + const url = useMemo( + () => (isWebViewOrigin(baseUrl) ? appendSessionId(baseUrl, sessionId) : baseUrl), + [baseUrl, sessionId], + ); /** * 우체통은 앱이 주입한 학생 토큰을 먼저 쓴다(웹 studentFetch). 주입이 없으면 웹이 diff --git a/ui/home/home-webview-screen.tsx b/ui/home/home-webview-screen.tsx index 5c79613..668f211 100644 --- a/ui/home/home-webview-screen.tsx +++ b/ui/home/home-webview-screen.tsx @@ -230,6 +230,10 @@ export function HomeWebViewScreen({ onError }: HomeWebViewScreenProps) { onHttpError={handleError} javaScriptEnabled domStorageEnabled + // Android 기본값(true)이면 target=_blank 가 onCreateWindow 로 가는데, + // onOpenWindow 핸들러가 없으면 화면에 붙지 않는 WebView 로 빨려들어가 링크가 죽는다. + // false 로 두면 같은 요청이 onShouldStartLoadWithRequest 를 타 iOS 와 같은 경로가 된다. + setSupportMultipleWindows={false} pullToRefreshEnabled allowsBackForwardNavigationGestures /> From 00423979c4e6a85701a0eac2bcbbe18c89de3898 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Tue, 22 Sep 2026 16:56:06 +0900 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20=EC=9B=B9=EB=B7=B0=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=20URL=20=ED=8C=90=EC=A0=95=EC=9D=84=20prefix=20?= =?UTF-8?q?=EB=8C=80=EC=8B=A0=20origin=20=EB=B9=84=EA=B5=90=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=94=EA=BE=BC=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- ui/home/home-webview-screen.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ui/home/home-webview-screen.tsx b/ui/home/home-webview-screen.tsx index 668f211..e76751c 100644 --- a/ui/home/home-webview-screen.tsx +++ b/ui/home/home-webview-screen.tsx @@ -2,7 +2,7 @@ import { useHomeWebViewPreloadContext } from '@/contexts/home-webview-preload-co import { useMixpanelContext } from '@/contexts/mixpanel-context'; import { useSubscribedClubsContext } from '@/contexts/subscribed-clubs-context'; import { ensureAccessToken } from '@/services/auth-token.service'; -import { appendSessionId, buildStudentTokenInjection, getWebViewUserAgent } from '@/utils/webview'; +import { appendSessionId, buildStudentTokenInjection, getWebViewUserAgent, isWebViewOrigin } from '@/utils/webview'; import Constants from 'expo-constants'; import { useRouter } from 'expo-router'; import * as WebBrowser from 'expo-web-browser'; @@ -174,8 +174,10 @@ export function HomeWebViewScreen({ onError }: HomeWebViewScreenProps) { const handleShouldStartLoadWithRequest = useCallback( (request: ShouldStartLoadRequest) => { - const baseOrigin = (process.env.EXPO_PUBLIC_WEBVIEW_URL ?? 'https://moadong.com').replace(/\/$/, ''); - if (request.url.startsWith('http') && !request.url.startsWith(baseOrigin)) { + // origin 을 문자열 prefix 로 판정하면 moadong.com.evil.com 이 내부로 통과한다. + // 그 페이지가 이 웹뷰에 뜨면 window.ReactNativeWebView.postMessage 로 브리지를 + // 그대로 쓸 수 있다(SUBSCRIBE_TOGGLE, OPEN_EXTERNAL_URL 등). 파싱해서 비교한다. + if (request.url.startsWith('http') && !isWebViewOrigin(request.url)) { // iOS: navigationType === 'click' 은 사용자가 직접 링크를 탭한 경우만 해당 // 초기 로드·서버 리다이렉트는 'other' 이므로 인터셉트하지 않음 // Android: navigationType이 항상 'other'이므로 loaded 상태로 구분 From d386506bc63b873c925fb43ca4945d722b39b2ca Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Tue, 29 Sep 2026 17:16:31 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20=ED=99=88=20=EC=9B=B9=EB=B7=B0?= =?UTF-8?q?=EC=9D=98=20=EC=99=B8=EB=B6=80=20=EB=A7=81=ED=81=AC=EB=A5=BC=20?= =?UTF-8?q?OS=20=EB=B8=8C=EB=9D=BC=EC=9A=B0=EC=A0=80=EB=A1=9C=20=EC=97=B0?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 외부 사이트를 앱 화면(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) --- ui/home/home-webview-screen.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ui/home/home-webview-screen.tsx b/ui/home/home-webview-screen.tsx index e76751c..8161ea0 100644 --- a/ui/home/home-webview-screen.tsx +++ b/ui/home/home-webview-screen.tsx @@ -185,14 +185,22 @@ export function HomeWebViewScreen({ onError }: HomeWebViewScreenProps) { ? request.navigationType === 'click' : loaded; if (isUserInitiated) { - router.push({ pathname: '/webview/[slug]', params: { slug: 'external', url: request.url } }); + // 외부 사이트는 OS 브라우저로 넘긴다. 앱 화면(WebView)에 띄우면 모아동 헤더가 + // 붙어 어디인지 구분이 안 되고, 앱 프로세스 안이라 그 페이지가 + // window.ReactNativeWebView 로 브리지를 쓸 수 있다. + // 배너·동아리 SNS·OPEN_EXTERNAL_URL 이 이미 같은 방식이다. + WebBrowser.openBrowserAsync(request.url, { + presentationStyle: WebBrowser.WebBrowserPresentationStyle.AUTOMATIC, + }).catch((error) => { + console.warn('[HomeWebView] 외부 링크 열기 실패:', request.url, error); + }); return false; } return true; } return true; }, - [router, loaded], + [loaded], ); // Android 하드웨어 뒤로가기: 웹뷰 히스토리가 있으면 웹뷰 back, 없으면 기본 동작(종료) From e9d99890fb76c2c729367389725fe6fb1b62df9b Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Tue, 29 Sep 2026 18:06:31 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20=EC=9B=B9=EB=B7=B0=20=EB=AA=A9?= =?UTF-8?q?=EC=A0=81=EC=A7=80=EB=A5=BC=20url=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=EB=A1=9C=20=EB=B0=9B=EC=A7=80=20=EC=95=8A=EB=8A=94?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 앞 커밋으로 홈의 외부 링크가 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) --- app/webview/[slug].tsx | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/app/webview/[slug].tsx b/app/webview/[slug].tsx index d7ad962..5b1e7f0 100644 --- a/app/webview/[slug].tsx +++ b/app/webview/[slug].tsx @@ -50,13 +50,11 @@ export default function WebViewScreen() { const { slug, path, - url: urlParam, title, hideHeader, } = useLocalSearchParams<{ slug?: string; path?: string; - url?: string; title?: string; hideHeader?: string; }>(); @@ -68,13 +66,16 @@ export default function WebViewScreen() { const config = pageConfig[slug || ""]; - const baseUrl = urlParam - ? String(urlParam) - : path - ? `${webviewUrl}${String(path).startsWith("/") ? "" : "/"}${String(path)}` - : config - ? (config.url ?? (config.path ? `${webviewUrl}${config.path}` : "")) - : ""; + /** + * 목적지는 path 또는 pageConfig 로만 정한다. 예전에는 url 파라미터를 그대로 받았는데, + * 그러면 moadongapp://webview/x?url=... 딥링크로 임의 사이트를 이 화면에 띄울 수 있다. + * 이 화면은 onMessage 가 붙어 있어 그 페이지가 앱 브리지를 그대로 쓴다. + */ + const baseUrl = path + ? `${webviewUrl}${String(path).startsWith("/") ? "" : "/"}${String(path)}` + : config + ? (config.url ?? (config.path ? `${webviewUrl}${config.path}` : "")) + : ""; /** * session_id 는 웹 Mixpanel 의 distinct_id 라 모아동 밖으로 나가면 안 된다. @@ -146,7 +147,7 @@ export default function WebViewScreen() { onNavigateWebview: handleNavigateWebview, }); - if (!config && !path && !urlParam) { + if (!config && !path) { return (