feat: FCM 푸시 알림 연동 및 딥링크 처리 - #166
Conversation
- react-grab 스크립트가 모바일 터치 이벤트를 인터셉트해 onClick이 실행 안 되는 문제 수정 - layout.tsx에서 WebView UA(PIKI_APP) 감지 시 스크립트 로드 차단 - LoginButtons.tsx에서 window.ReactNativeWebView 직접 사용으로 변경
- iOS CookieManager.set에 useWebKit: true 추가로 WKHTTPCookieStore에 직접 동기화 - WebBridge에 injectCookies 추가, 로그인 성공 직후 document.cookie에 토큰 주입 - useWebBridgeMessage 디버그 핸들러 제거
- next.config.js rewrite 제거 - /api/v1/[...path] Route Handler 추가로 origin 헤더 제거 후 백엔드 프록시 - LAN IP 환경에서 백엔드 CORS 거부 문제 해결
- 로그인 전 FCM 토큰 등록 시도는 미인증으로 실패 - SOCIAL_LOGIN_SUCCESS 수신 후 쿠키 세팅 직후 WEB_REQ_PUSH_PERMISSION_STATUS 재요청
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
More reviews will be available in 54 minutes and 24 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
Walkthrough앱·웹 WebBridge 규약을 WEB_REQ_/APP_RES_로 전환하고, 앱에 FCM 권한·토큰·리스너를 추가해 웹뷰로 상태·토큰을 전송하며 웹에서 토큰 등록·딥링크 라우팅·알림 UI를 연동합니다. Changes푸시 연동 및 브리지 프로토콜 전환
웹: 토큰 API·프록시·알림 UI
Sequence Diagram(s) sequenceDiagram
participant App as Mobile App
participant WebView as App WebView (in-app)
participant WebServer as Backend
participant FCM as Firebase Cloud Messaging
App->>FCM: getToken() (initializePushNotification)
App->>WebView: postMessage(APP_RES_PUSH_PERMISSION_STATUS { isEnabled, token, deviceId })
WebView->>WebServer: POST /api/v1/fcm/tokens (postFcmToken)
FCM->>App: Deliver notification (foreground/background)
App->>WebView: postMessage(APP_REQ_DEEP_LINK { payload })
WebView->>WebView: useDeepLink -> router.push(route)
Estimated code review effort 🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/app/hooks/useWebviewCookieSync.ts (1)
16-35:⚠️ Potential issue | 🟠 Major | ⚡ Quick win쿠키 동기화 실패가 WebView 마운트를 영구히 막습니다.
여기서
TokenStorage.get*나CookieManager.set가 한 번이라도 reject되면 Line 32에 도달하지 못합니다. 그런데apps/app/app/index.tsxLine 82-93은isSynced가true일 때만 WebView를 렌더링하므로, 실패 시 앱이 빈 화면으로 고정됩니다. 동기화 실패는 로그만 남기고,setIsSynced(true)는finally에서 보장해야 합니다.변경 예시
useEffect(() => { const sync = async () => { - const accessToken = await TokenStorage.getAccessToken(); - const refreshToken = await TokenStorage.getRefreshToken(); - - // iOS WKWebView는 WKHTTPCookieStore를 사용하므로 useWebKit: true 필요 - const useWebKit = Platform.OS === 'ios'; - - if (accessToken) { - await CookieManager.set(WEB_URL, { name: 'access_token', value: accessToken, path: '/' }, useWebKit); - } - - if (refreshToken) { - await CookieManager.set(WEB_URL, { name: 'refresh_token', value: refreshToken, path: '/' }, useWebKit); - } - - setIsSynced(true); + try { + const accessToken = await TokenStorage.getAccessToken(); + const refreshToken = await TokenStorage.getRefreshToken(); + + // iOS WKWebView는 WKHTTPCookieStore를 사용하므로 useWebKit: true 필요 + const useWebKit = Platform.OS === 'ios'; + + if (accessToken) { + await CookieManager.set( + WEB_URL, + { name: 'access_token', value: accessToken, path: '/' }, + useWebKit + ); + } + + if (refreshToken) { + await CookieManager.set( + WEB_URL, + { name: 'refresh_token', value: refreshToken, path: '/' }, + useWebKit + ); + } + } catch (error) { + console.error('[WEBVIEW] 쿠키 동기화 실패', error); + } finally { + setIsSynced(true); + } }; - sync(); + void sync(); }, []);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/app/hooks/useWebviewCookieSync.ts` around lines 16 - 35, The sync() effect can reject (TokenStorage.getAccessToken/getRefreshToken or CookieManager.set) and never call setIsSynced(true), blocking WebView; wrap the await calls in try/catch and move setIsSynced(true) into a finally block so sync always marks completion, logging errors in catch; update the useEffect's sync function (the async function declared inside useEffect) to try { ...await TokenStorage.getAccessToken(); ... await CookieManager.set(...) } catch (err) { console.error or processLogger.error(...) } finally { setIsSynced(true); } so isSynced is guaranteed even on failure.
🧹 Nitpick comments (7)
apps/web/src/utils/pushNotificationRoute.ts (1)
5-5: ⚡ Quick win반환 타입 명시 권장
함수가 조건에 따라
undefined를 반환할 수 있으므로, 명시적 반환 타입 선언이 코드 가독성과 타입 안정성을 높입니다.♻️ 반환 타입 추가
-export const getPushNotificationRoute = (payload: DeepLinkPayloadT) => { +export const getPushNotificationRoute = (payload: DeepLinkPayloadT): string | undefined => {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/utils/pushNotificationRoute.ts` at line 5, getPushNotificationRoute currently may return undefined in some branches but lacks an explicit return type; update the function signature to declare the precise return type (e.g. export const getPushNotificationRoute = (payload: DeepLinkPayloadT): string | undefined => { ... }) so callers and TypeScript know it can return a string or undefined, and adjust any related types/usages if needed.apps/web/src/app/notification/_components/NotificationContent.tsx (1)
36-38: 푸시 권한 배너 조건의null/undefined처리 재검토
usePushPermission의isPushEnabled는useState<boolean | null>(null)로 초기화되고, 앱 응답 수신 시message.payload.isEnabled(boolean)만 설정하므로undefined는 발생하지 않아isPushEnabled === false조건 자체는 의도대로 동작합니다. 다만 응답이 지연/미수신되어null이 유지되면 배너가 계속 표시되지 않으니,null(대기) 상태에 대한 UI/대체 로직(로딩·타임아웃·폴백) 필요성을 검토하세요.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/app/notification/_components/NotificationContent.tsx` around lines 36 - 38, The banner rendering currently checks isPushEnabled === false which treats null (initial/pending) the same as allowed, so review usePushPermission and change NotificationContent to handle the null/pending state explicitly: use the isPushEnabled value from usePushPermission and when it is null show a loading/placeholder or implement a timeout/fallback before showing PushDisabledBanner; keep rendering PushDisabledBanner (with onOpenNotificationSettings) only when isPushEnabled === false and isWebview() is true, and ensure any timeout or fallback updates isPushEnabled (or a new local pending flag) so the banner isn’t shown prematurely or hidden indefinitely.apps/web/src/app/notification/_hooks/usePushPermission.ts (1)
10-21: ⚖️ Poor tradeoff선택적 개선: 앱 응답 타임아웃 처리 고려
isPushEnabled상태가null로 초기화된 후, 앱이APP_RES_PUSH_PERMISSION_STATUS메시지로 응답하기를 무한정 대기합니다. 앱이 응답하지 않거나 지연되는 경우, 사용자에게 로딩 상태가 지속되거나 UI가 렌더링되지 않을 수 있습니다.필요하다면 타임아웃을 추가하여 일정 시간 후 기본값(예:
false)으로 폴백하거나, 에러 상태를 노출하는 것을 고려하세요.💡 타임아웃 예시 (선택사항)
useEffect(() => { if (!isWebview) return; WebBridge.postMessage({ type: WEBBRIDGE_MESSAGE_TYPE.WEB_REQ_PUSH_PERMISSION_STATUS }); + + // 3초 후에도 응답 없으면 기본값으로 폴백 + const timeoutId = setTimeout(() => { + setIsPushEnabled(prev => prev === null ? false : prev); + }, 3000); + + return () => clearTimeout(timeoutId); }, [isWebview]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/app/notification/_hooks/usePushPermission.ts` around lines 10 - 21, The hook usePushPermission currently leaves isPushEnabled as null indefinitely if the app never replies to WEBBRIDGE_MESSAGE_TYPE.APP_RES_PUSH_PERMISSION_STATUS; add a timeout fallback: when the hook mounts (and only if isWebview is true), start a timer (e.g., 3–5s) that calls setIsPushEnabled(false) if no APP_RES_PUSH_PERMISSION_STATUS arrives, clear the timer when a valid message is received inside the useWebBridgeMessage callback (the branch that calls setIsPushEnabled) and also clear the timer in the hook cleanup to avoid leaks; ensure the timeout is only installed once and respects existing message handling logic.apps/web/src/app/login/_components/LoginButtons.tsx (1)
18-42: ⚡ Quick winWebBridge 유틸리티를 사용하여 일관성과 타입 안전성을 개선하세요.
handleKakaoLogin과handleGoogleLogin이window.ReactNativeWebView에 직접 접근하여 메시지를 전송하고 있습니다. 이는 다음 문제를 야기합니다:
apps/web/src/utils/webBridge.ts의WebBridge.postMessage유틸리티가 제공하는 타입 가드(isRNWebViewWindow)를 우회합니다.- 인라인 타입 단언이 중복되어 있습니다(19줄, 32줄).
- 다른 파일(
useNativeLoginResult,usePushPermission)에서는WebBridge.postMessage를 사용하는데, 여기서만 직접 접근하여 패턴이 불일치합니다.♻️ WebBridge 유틸리티 사용 제안
const handleKakaoLogin = () => { - const rnWebView = (window as Window & { ReactNativeWebView?: { postMessage: (msg: string) => void } }).ReactNativeWebView; - - if (rnWebView) { - rnWebView.postMessage( - JSON.stringify({ type: WEBBRIDGE_MESSAGE_TYPE.REQUEST_SOCIAL_LOGIN, payload: { provider: 'kakao' } }) - ); + if (isWebview()) { + WebBridge.postMessage({ + type: WEBBRIDGE_MESSAGE_TYPE.REQUEST_SOCIAL_LOGIN, + payload: { provider: 'kakao' } + }); return; } getAuthUrl('kakao').then(({ url }) => { window.location.href = url; }); }; const handleGoogleLogin = () => { - const rnWebView = (window as Window & { ReactNativeWebView?: { postMessage: (msg: string) => void } }).ReactNativeWebView; - - if (rnWebView) { - rnWebView.postMessage( - JSON.stringify({ type: WEBBRIDGE_MESSAGE_TYPE.REQUEST_SOCIAL_LOGIN, payload: { provider: 'google' } }) - ); + if (isWebview()) { + WebBridge.postMessage({ + type: WEBBRIDGE_MESSAGE_TYPE.REQUEST_SOCIAL_LOGIN, + payload: { provider: 'google' } + }); return; } getAuthUrl('google').then(({ url }) => { window.location.href = url; }); };필요한 import 추가:
+ import { isWebview, WebBridge } from '`@/utils/webBridge`';🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/app/login/_components/LoginButtons.tsx` around lines 18 - 42, Replace direct window.ReactNativeWebView access in handleKakaoLogin and handleGoogleLogin with the WebBridge utility: import WebBridge (and use its isRNWebViewWindow type guard if needed) and call WebBridge.postMessage with the same payload ({ type: WEBBRIDGE_MESSAGE_TYPE.REQUEST_SOCIAL_LOGIN, payload: { provider: 'kakao' } }) for kakao and provider: 'google' for google; keep the existing fallback to getAuthUrl('kakao') / getAuthUrl('google') and return early after posting so behavior is unchanged, removing the duplicated inline window type assertions.packages/core/src/types/pushNotification.ts (1)
45-55: ⚖️ Poor tradeoff
DeepLinkPayloadT의 타입 정밀도 개선을 고려해보세요.현재 두 번째 variant에서
tournamentId가 optional로 정의되어 있지만,kind값에 따라 실제로는 다음과 같은 관계를 가질 것으로 예상됩니다:
kind: 'WISH'일 때 →tournamentId불필요kind: 'TOURNAMENT'일 때 →tournamentId필수타입을 더 정확하게 표현하려면 다음과 같이 분리할 수 있습니다:
♻️ 더 정밀한 타입 정의 제안
export type DeepLinkPayloadT = | { type: 'TOURNAMENT_JOINED' | 'TOURNAMENT_ITEM_ADDED'; refId: number; } + | { + type: 'ITEM_PARSING_COMPLETED' | 'ITEM_PARSING_FAILED'; + refId: number; + kind: 'WISH'; + } | { type: 'ITEM_PARSING_COMPLETED' | 'ITEM_PARSING_FAILED'; refId: number; - kind: 'WISH' | 'TOURNAMENT'; - tournamentId?: number; + kind: 'TOURNAMENT'; + tournamentId: number; };단, 현재 구조가 의도적으로 유연성을 위해 설계된 것이라면 변경하지 않아도 됩니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/types/pushNotification.ts` around lines 45 - 55, The DeepLinkPayloadT union is imprecise because tournamentId is optional regardless of kind; update DeepLinkPayloadT so the parsing-related variant is split into two distinct types based on kind (e.g., one variant with kind: 'WISH' that omits tournamentId and another with kind: 'TOURNAMENT' that requires tournamentId) while keeping the other top-level variants (TOURNAMENT_JOINED, TOURNAMENT_ITEM_ADDED) unchanged; locate and change the union definition for DeepLinkPayloadT to enforce tournamentId presence only when kind === 'TOURNAMENT'.apps/web/src/app/archive/_components/wish-grid/index.tsx (1)
22-46: 💤 Low valueDelete mode에서 중복된 key prop을 정리하는 것을 고려해보세요.
현재 Line 22에서
card상수에key={item.id}를 추가했는데, delete mode일 때는 Line 28의button에도key={item.id}가 있어 다음과 같은 구조가 됩니다:<button key={item.id}> <WishCard key={item.id} ... /> </button>React의 reconciliation에서는 map의 직접 자식(여기서는 button)에 있는 key만 사용되며, 중첩된 WishCard의 key는 무시됩니다. 기능상 문제는 없지만 불필요한 prop이 전달되고 있습니다.
더 명확한 구조를 위해 다음을 고려해보세요:
- card 상수 정의 시 key를 제거
- Delete mode가 아닐 때만 WishCard에 직접 key 부여
- Delete mode일 때는 button의 key만 유지
♻️ 리팩터링 제안
-const card = <WishCard key={item.id} name={item.name} price={item.price} imageUrl={item.imageUrl} />; +const card = <WishCard name={item.name} price={item.price} imageUrl={item.imageUrl} />; if (isDeleteMode) { const isSelected = selectedIds?.has(item.id) ?? false; return ( <button key={item.id} ... > {card} ... </button> ); } -return card; +return <WishCard key={item.id} name={item.name} price={item.price} imageUrl={item.imageUrl} />;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/app/archive/_components/wish-grid/index.tsx` around lines 22 - 46, Remove the redundant key from the shared card constant and ensure keys are only applied to the map's direct child: stop setting key on the card const, render WishCard with key={item.id} only in the non-delete branch (when isDeleteMode is false), and keep the key on the button only in the isDeleteMode branch; update usages around the card/WishCard/button in the isDeleteMode, selectedIds, and onToggleSelect logic accordingly.apps/app/utils/webBridge.ts (1)
26-36: ⚡ Quick win성공 경로의
console.log는 제거해 주세요.이 유틸은 로그인 직후 반복 호출될 수 있어서 프로덕션 로그를 계속 오염시킵니다. 성공 로그가 꼭 필요하면
__DEV__에서만 남기고, 기본 경로에서는 제거하는 편이 안전합니다.변경 예시
target.injectJavaScript(`${js}\ntrue;`); - console.log('[WEBVIEW] 쿠키 주입 완료:', Object.keys(cookies).join(', ')); },As per coding guidelines, "No
console.logallowed; onlywarnanderrorare permitted".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/app/utils/webBridge.ts` around lines 26 - 36, Remove the success console.log in injectCookies to avoid noisy production logs: in the injectCookies method (webviewRef, target, js, target.injectJavaScript), either delete the console.log('[WEBVIEW] 쿠키 주입 완료:...') line or guard it so it only runs in development (e.g. wrap it with a __DEV__ conditional); keep the existing console.warn for missing target and leave error/warn-only logging behavior intact.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/app/app.json`:
- Line 16: Remove the inline comment from the "aps-environment" entry in
app.json (comments are invalid JSON) and instead drive its value from an
environment variable: add logic in app.config.ts to read
process.env.EXPO_PUBLIC_APS_ENVIRONMENT (defaulting to "development") and inject
that value into the exported config so the "aps-environment" key in app.json is
populated at build time; ensure the unique symbol "aps-environment" is updated
via app.config.ts and document that production builds must set
EXPO_PUBLIC_APS_ENVIRONMENT=production.
In `@apps/app/app/index.tsx`:
- Around line 40-66: handleWebMessage currently only handles the new WEB_REQ_*
protocol and will drop legacy messages during rollout; update handleWebMessage
to provide a compatibility layer: accept legacy message type names (aliases) or
perform a protocol handshake when WEBBRIDGE_MESSAGE_TYPE.WEB_REQ_READY (inspect
payload.version or payload.type) and either map old types to the new handlers
(map legacy share/image/push message names to sendShareIntent,
handleOpenImagePicker, handleRequestPushPermission, syncPushStatusToWeb,
handleLogin, Linking.openSettings) or reject unsupported versions with an
explicit response so web and app can detect incompatibility; implement the
mapping and handshake logic inside handleWebMessage so sendShareIntent,
handleOpenImagePicker, handleLogin, syncPushStatusToWeb and
handleRequestPushPermission are invoked for both new and legacy message formats.
In `@apps/app/utils/pushNotification.ts`:
- Around line 135-141: sendDeepLink currently does Number(data.refId) without
validation which can produce NaN; update the sendDeepLink function to parse and
validate refId (e.g., use Number or parseInt and check !isNaN(refId) &&
isFinite(refId) and optionally refId > 0) before posting the WebBridge message;
if validation fails, bail out (or log) instead of sending a payload with NaN —
reference the sendDeepLink function, the
WEBBRIDGE_MESSAGE_TYPE.APP_REQ_DEEP_LINK payload creation, and DeepLinkPayloadT
when implementing the check.
- Around line 143-149: The code blindly asserts remoteMessage.data as
Record<string,string>|undefined in the onNotificationOpenedApp and
getInitialNotification handlers; instead add runtime validation inside the
callbacks: check remoteMessage is truthy, then verify remoteMessage.data is an
object, iterate its keys and keep only entries whose values are strings (or
stringify non-primitive values if desired), build a safe Record<string,string>
and pass that to sendDeepLink; update the handlers referenced
(onNotificationOpenedApp, getInitialNotification, sendDeepLink) so they never
receive an unchecked assertion of remoteMessage.data.
In `@apps/web/src/app/api/v1/`[...path]/route.ts:
- Line 4: The code reads process.env.NEXT_PUBLIC_API_URL into the constant
API_URL without validating it, which yields bad URLs like "undefined/..." and
runtime fetch failures; update route.ts to validate API_URL at startup or before
use (reference the API_URL constant and any functions building URLs like the
fetch path in this module) and if it's missing either throw a clear error during
initialization or return an appropriate HTTP error response (e.g., 500) with a
descriptive message; alternatively provide a safe fallback or move the variable
to next.config.js env to guarantee it's present at build time—ensure the
validation is deterministic and logs the missing variable name so failures are
obvious.
In `@apps/web/src/app/login/_hooks/usePostGuestLogin.ts`:
- Around line 24-26: 로그에 민감한 accessToken을 직접 출력하고 있는 usePostGuestLogin의
development-only logging (process.env.NODE_ENV === 'development' block)을 제거하거나
안전하게 변경하세요: 대신 data.accessToken 전체를 출력하지 말고 존재 여부만 로그하거나 일부를 마스킹(예: 앞/뒤 4자만
표시)하도록 변경하고, 해당 logic을 재사용 가능한 helper(e.g., maskToken)로 옮겨 usePostGuestLogin에서
maskToken(data.accessToken) 또는 !!data.accessToken 로 로그하도록 구현하세요.
In `@apps/web/src/app/notification/_components/PushDisabledBanner.tsx`:
- Around line 3-5: Rename the generic Props type to follow the component naming
convention: change the type name from Props to PushDisabledBannerProps and
update all references (e.g., the component's props annotation and any
destructuring/usages) in PushDisabledBanner (the PushDisabledBanner.tsx file) to
use PushDisabledBannerProps so the component signature and any imports/exports
remain consistent.
In `@apps/web/src/consts/api.ts`:
- Line 26: Search the repo for usages of the FCM_PUSH constant and the literal
'/api/v1/dev/fcm/push' to confirm whether this dev endpoint can ever be invoked
in production; if it is truly dev-only, move or rename the constant (e.g.,
DEV_FCM_PUSH) out of shared production constants and/or wrap any consumer code
to select the endpoint based on environment (process.env.NODE_ENV or isDev flag)
so production cannot call the dev URL; if the constant is unused, remove it;
alternatively, add a runtime guard in code paths that would use FCM_PUSH (throw
or log and fall back to a safe prod endpoint when not in dev) to ensure the dev
endpoint cannot be used in production.
---
Outside diff comments:
In `@apps/app/hooks/useWebviewCookieSync.ts`:
- Around line 16-35: The sync() effect can reject
(TokenStorage.getAccessToken/getRefreshToken or CookieManager.set) and never
call setIsSynced(true), blocking WebView; wrap the await calls in try/catch and
move setIsSynced(true) into a finally block so sync always marks completion,
logging errors in catch; update the useEffect's sync function (the async
function declared inside useEffect) to try { ...await
TokenStorage.getAccessToken(); ... await CookieManager.set(...) } catch (err) {
console.error or processLogger.error(...) } finally { setIsSynced(true); } so
isSynced is guaranteed even on failure.
---
Nitpick comments:
In `@apps/app/utils/webBridge.ts`:
- Around line 26-36: Remove the success console.log in injectCookies to avoid
noisy production logs: in the injectCookies method (webviewRef, target, js,
target.injectJavaScript), either delete the console.log('[WEBVIEW] 쿠키 주입
완료:...') line or guard it so it only runs in development (e.g. wrap it with a
__DEV__ conditional); keep the existing console.warn for missing target and
leave error/warn-only logging behavior intact.
In `@apps/web/src/app/archive/_components/wish-grid/index.tsx`:
- Around line 22-46: Remove the redundant key from the shared card constant and
ensure keys are only applied to the map's direct child: stop setting key on the
card const, render WishCard with key={item.id} only in the non-delete branch
(when isDeleteMode is false), and keep the key on the button only in the
isDeleteMode branch; update usages around the card/WishCard/button in the
isDeleteMode, selectedIds, and onToggleSelect logic accordingly.
In `@apps/web/src/app/login/_components/LoginButtons.tsx`:
- Around line 18-42: Replace direct window.ReactNativeWebView access in
handleKakaoLogin and handleGoogleLogin with the WebBridge utility: import
WebBridge (and use its isRNWebViewWindow type guard if needed) and call
WebBridge.postMessage with the same payload ({ type:
WEBBRIDGE_MESSAGE_TYPE.REQUEST_SOCIAL_LOGIN, payload: { provider: 'kakao' } })
for kakao and provider: 'google' for google; keep the existing fallback to
getAuthUrl('kakao') / getAuthUrl('google') and return early after posting so
behavior is unchanged, removing the duplicated inline window type assertions.
In `@apps/web/src/app/notification/_components/NotificationContent.tsx`:
- Around line 36-38: The banner rendering currently checks isPushEnabled ===
false which treats null (initial/pending) the same as allowed, so review
usePushPermission and change NotificationContent to handle the null/pending
state explicitly: use the isPushEnabled value from usePushPermission and when it
is null show a loading/placeholder or implement a timeout/fallback before
showing PushDisabledBanner; keep rendering PushDisabledBanner (with
onOpenNotificationSettings) only when isPushEnabled === false and isWebview() is
true, and ensure any timeout or fallback updates isPushEnabled (or a new local
pending flag) so the banner isn’t shown prematurely or hidden indefinitely.
In `@apps/web/src/app/notification/_hooks/usePushPermission.ts`:
- Around line 10-21: The hook usePushPermission currently leaves isPushEnabled
as null indefinitely if the app never replies to
WEBBRIDGE_MESSAGE_TYPE.APP_RES_PUSH_PERMISSION_STATUS; add a timeout fallback:
when the hook mounts (and only if isWebview is true), start a timer (e.g., 3–5s)
that calls setIsPushEnabled(false) if no APP_RES_PUSH_PERMISSION_STATUS arrives,
clear the timer when a valid message is received inside the useWebBridgeMessage
callback (the branch that calls setIsPushEnabled) and also clear the timer in
the hook cleanup to avoid leaks; ensure the timeout is only installed once and
respects existing message handling logic.
In `@apps/web/src/utils/pushNotificationRoute.ts`:
- Line 5: getPushNotificationRoute currently may return undefined in some
branches but lacks an explicit return type; update the function signature to
declare the precise return type (e.g. export const getPushNotificationRoute =
(payload: DeepLinkPayloadT): string | undefined => { ... }) so callers and
TypeScript know it can return a string or undefined, and adjust any related
types/usages if needed.
In `@packages/core/src/types/pushNotification.ts`:
- Around line 45-55: The DeepLinkPayloadT union is imprecise because
tournamentId is optional regardless of kind; update DeepLinkPayloadT so the
parsing-related variant is split into two distinct types based on kind (e.g.,
one variant with kind: 'WISH' that omits tournamentId and another with kind:
'TOURNAMENT' that requires tournamentId) while keeping the other top-level
variants (TOURNAMENT_JOINED, TOURNAMENT_ITEM_ADDED) unchanged; locate and change
the union definition for DeepLinkPayloadT to enforce tournamentId presence only
when kind === 'TOURNAMENT'.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 1a9b43de-5398-4f3d-834c-c3b937975d15
⛔ Files ignored due to path filters (2)
apps/app/assets/images/icon.pngis excluded by!**/*.pngpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (48)
apps/app/.gitignoreapps/app/app.config.tsapps/app/app.jsonapps/app/app/_layout.tsxapps/app/app/index.tsxapps/app/components/PushNotificationProvider.tsxapps/app/firebase.jsonapps/app/hooks/useShareIntent.tsapps/app/hooks/useSocialLogin.tsapps/app/hooks/useWebBridgeMessage.tsapps/app/hooks/useWebviewCookieSync.tsapps/app/index.jsapps/app/package.jsonapps/app/utils/handleImage.tsapps/app/utils/pushNotification.tsapps/app/utils/webBridge.tsapps/web/next.config.jsapps/web/src/apis/client.tsapps/web/src/apis/deleteFcmToken.tsapps/web/src/apis/postFcmToken.tsapps/web/src/app/_actions/logout.tsapps/web/src/app/api/v1/[...path]/route.tsapps/web/src/app/archive/_components/wish-grid/index.tsxapps/web/src/app/archive/_hooks/useShareIntentWish.tsapps/web/src/app/layout.tsxapps/web/src/app/login/_components/LoginButtons.tsxapps/web/src/app/login/_hooks/usePostGuestLogin.tsapps/web/src/app/notification/_components/NotificationContent.tsxapps/web/src/app/notification/_components/NotificationEmptyState.tsxapps/web/src/app/notification/_components/PushDisabledBanner.tsxapps/web/src/app/notification/_hooks/usePushPermission.tsapps/web/src/app/notification/page.tsxapps/web/src/components/Providers.tsxapps/web/src/components/header/index.tsxapps/web/src/consts/api.tsapps/web/src/consts/route.tsapps/web/src/hooks/useDeepLink.tsapps/web/src/hooks/useFcmTokenSync.tsapps/web/src/hooks/useImagePicker.tsapps/web/src/hooks/useNativeLoginResult.tsapps/web/src/utils/pushNotificationRoute.tsapps/web/src/utils/webBridge.tspackages/core/src/consts/webBridge.tspackages/core/src/index.tspackages/core/src/types/image.tspackages/core/src/types/pushNotification.tspackages/core/src/types/shareIntent.tspackages/core/src/types/webBridge.ts
💤 Files with no reviewable changes (2)
- apps/web/next.config.js
- apps/app/hooks/useWebBridgeMessage.ts
| "supportsTablet": false, | ||
| "bundleIdentifier": "day.no30s.piki", | ||
| "entitlements": { | ||
| "aps-environment": "development", // NOTE: 배포할때 'production'으로 변경 |
There was a problem hiding this comment.
JSON 구문 오류: 주석이 허용되지 않습니다.
표준 JSON은 주석을 지원하지 않습니다. Line 16의 인라인 주석(// NOTE: 배포할때 'production'으로 변경)은 파싱 오류를 일으킬 수 있습니다.
🔧 권장 수정 방안
주석을 제거하고, 환경별 설정은 app.config.ts에서 환경변수로 제어하도록 변경하세요:
app.config.ts:
ios: {
...config.ios,
+ entitlements: {
+ ...config.ios?.entitlements,
+ 'aps-environment': process.env.EXPO_PUBLIC_APS_ENVIRONMENT ?? 'development',
+ },
googleServicesFile:
process.env.GOOGLE_SERVICES_INFO_PLIST ?? LOCAL_GOOGLE_SERVICES_INFO_PLIST,
},app.json:
"entitlements": {
- "aps-environment": "development", // NOTE: 배포할때 'production'으로 변경
+ "aps-environment": "development",
"com.apple.security.application-groups": ["group.day.no30s.piki"]
},프로덕션 빌드 시 환경변수로 EXPO_PUBLIC_APS_ENVIRONMENT=production을 설정하세요.
🧰 Tools
🪛 Biome (2.4.16)
[error] 16-16: Expected a property but instead found '// NOTE: 배포할때 'production'으로 변경'.
(parse)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/app/app.json` at line 16, Remove the inline comment from the
"aps-environment" entry in app.json (comments are invalid JSON) and instead
drive its value from an environment variable: add logic in app.config.ts to read
process.env.EXPO_PUBLIC_APS_ENVIRONMENT (defaulting to "development") and inject
that value into the exported config so the "aps-environment" key in app.json is
populated at build time; ensure the unique symbol "aps-environment" is updated
via app.config.ts and document that production builds must set
EXPO_PUBLIC_APS_ENVIRONMENT=production.
Source: Linters/SAST tools
| const handleWebMessage = useCallback( | ||
| async (message: WebBridgeMessageT) => { | ||
| switch (message.type) { | ||
| case WEBBRIDGE_MESSAGE_TYPE.WEB_READY: { | ||
| case WEBBRIDGE_MESSAGE_TYPE.WEB_REQ_READY: { | ||
| const { type } = message.payload; | ||
| if (type === WEB_READY_MESSAGE_TYPE.SHARE_INTENT) sendShareIntent(); | ||
| if (type === WEB_REQ_READY_PAYLOAD_TYPE.SHARE_INTENT) sendShareIntent(); | ||
| return; | ||
| } | ||
| case WEBBRIDGE_MESSAGE_TYPE.OPEN_IMAGE_PICKER: | ||
|
|
||
| case WEBBRIDGE_MESSAGE_TYPE.WEB_REQ_OPEN_IMAGE_PICKER: | ||
| await handleOpenImagePicker(message.payload); | ||
| return; | ||
| case WEBBRIDGE_MESSAGE_TYPE.REQUEST_SOCIAL_LOGIN: | ||
| await handleLogin(message.payload.provider); | ||
| return; | ||
|
|
||
| case WEBBRIDGE_MESSAGE_TYPE.WEB_REQ_PUSH_PERMISSION_STATUS: | ||
| await syncPushStatusToWeb(); | ||
| return; | ||
|
|
||
| case WEBBRIDGE_MESSAGE_TYPE.WEB_REQ_PUSH_PERMISSION: | ||
| await handleRequestPushPermission(); | ||
| return; | ||
|
|
||
| case WEBBRIDGE_MESSAGE_TYPE.WEB_REQ_OPEN_NOTIFICATION_SETTINGS: | ||
| await Linking.openSettings(); | ||
| return; |
There was a problem hiding this comment.
브리지 프로토콜을 전면 교체했는데 호환 계층이 없습니다.
Line 43-65는 새 WEB_REQ_* 메시지만 처리합니다. 그런데 이 화면은 Line 22-24, Line 87에서 원격 웹 URL을 로드하고, 웹 쪽도 apps/web/src/app/archive/_hooks/useShareIntentWish.ts Line 59-64 / apps/web/src/hooks/useImagePicker.ts Line 123-129에서 새 타입만 전송합니다. 이 구조면 웹 선배포나 앱 구버전 잔존 시 공유 인텐트, 이미지 피커, 푸시 권한 요청이 바로 끊깁니다. 과도기 동안은 구 타입 alias를 함께 받거나, 최소한 프로토콜 버전 handshake로 미지원 조합을 차단하는 계층이 필요합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/app/app/index.tsx` around lines 40 - 66, handleWebMessage currently only
handles the new WEB_REQ_* protocol and will drop legacy messages during rollout;
update handleWebMessage to provide a compatibility layer: accept legacy message
type names (aliases) or perform a protocol handshake when
WEBBRIDGE_MESSAGE_TYPE.WEB_REQ_READY (inspect payload.version or payload.type)
and either map old types to the new handlers (map legacy share/image/push
message names to sendShareIntent, handleOpenImagePicker,
handleRequestPushPermission, syncPushStatusToWeb, handleLogin,
Linking.openSettings) or reject unsupported versions with an explicit response
so web and app can detect incompatibility; implement the mapping and handshake
logic inside handleWebMessage so sendShareIntent, handleOpenImagePicker,
handleLogin, syncPushStatusToWeb and handleRequestPushPermission are invoked for
both new and legacy message formats.
| const sendDeepLink = (data: Record<string, string> | undefined) => { | ||
| if (!data?.type || !data?.refId) return; | ||
| WebBridge.postMessage({ | ||
| type: WEBBRIDGE_MESSAGE_TYPE.APP_REQ_DEEP_LINK, | ||
| payload: { type: data.type, refId: Number(data.refId) } as DeepLinkPayloadT, | ||
| }); | ||
| }; |
There was a problem hiding this comment.
refId 숫자 변환 시 유효성 검증이 필요합니다.
Line 139에서 data.refId를 Number()로 변환하지만, 값이 유효한 숫자 문자열인지 검증하지 않습니다. 잘못된 형식의 데이터가 들어올 경우 NaN이 전달되어 라우팅이 실패할 수 있습니다.
🛡️ 권장 수정
const sendDeepLink = (data: Record<string, string> | undefined) => {
- if (!data?.type || !data?.refId) return;
+ const refId = Number(data?.refId);
+ if (!data?.type || !data?.refId || Number.isNaN(refId)) return;
+
WebBridge.postMessage({
type: WEBBRIDGE_MESSAGE_TYPE.APP_REQ_DEEP_LINK,
- payload: { type: data.type, refId: Number(data.refId) } as DeepLinkPayloadT,
+ payload: { type: data.type, refId } as DeepLinkPayloadT,
});
};🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/app/utils/pushNotification.ts` around lines 135 - 141, sendDeepLink
currently does Number(data.refId) without validation which can produce NaN;
update the sendDeepLink function to parse and validate refId (e.g., use Number
or parseInt and check !isNaN(refId) && isFinite(refId) and optionally refId > 0)
before posting the WebBridge message; if validation fails, bail out (or log)
instead of sending a payload with NaN — reference the sendDeepLink function, the
WEBBRIDGE_MESSAGE_TYPE.APP_REQ_DEEP_LINK payload creation, and DeepLinkPayloadT
when implementing the check.
| const unsubscribeOpenedApp = onNotificationOpenedApp(messaging, remoteMessage => { | ||
| sendDeepLink(remoteMessage.data as Record<string, string> | undefined); | ||
| }); | ||
|
|
||
| void getInitialNotification(messaging).then(remoteMessage => { | ||
| if (remoteMessage) sendDeepLink(remoteMessage.data as Record<string, string> | undefined); | ||
| }); |
There was a problem hiding this comment.
FCM remoteMessage.data 타입 안전성을 개선하세요.
Line 144와 148에서 remoteMessage.data를 Record<string, string> | undefined로 타입 단언하지만, 실제 런타임 타입을 검증하지 않습니다. FCM 메시지 페이로드는 외부에서 제공되므로, 타입 단언 대신 런타임 검증이 필요합니다.
🔒 권장 수정
+ const isValidDeepLinkData = (
+ data: unknown
+ ): data is Record<string, string> => {
+ return (
+ typeof data === 'object' &&
+ data !== null &&
+ 'type' in data &&
+ 'refId' in data &&
+ typeof data.type === 'string' &&
+ typeof data.refId === 'string'
+ );
+ };
+
const sendDeepLink = (data: Record<string, string> | undefined) => {
- if (!data?.type || !data?.refId) return;
+ if (!isValidDeepLinkData(data)) return;
+ const refId = Number(data.refId);
+ if (Number.isNaN(refId)) return;
+
WebBridge.postMessage({
type: WEBBRIDGE_MESSAGE_TYPE.APP_REQ_DEEP_LINK,
- payload: { type: data.type, refId: Number(data.refId) } as DeepLinkPayloadT,
+ payload: { type: data.type, refId },
});
};
const unsubscribeOpenedApp = onNotificationOpenedApp(messaging, remoteMessage => {
- sendDeepLink(remoteMessage.data as Record<string, string> | undefined);
+ sendDeepLink(remoteMessage.data);
});
void getInitialNotification(messaging).then(remoteMessage => {
- if (remoteMessage) sendDeepLink(remoteMessage.data as Record<string, string> | undefined);
+ if (remoteMessage) sendDeepLink(remoteMessage.data);
});🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/app/utils/pushNotification.ts` around lines 143 - 149, The code blindly
asserts remoteMessage.data as Record<string,string>|undefined in the
onNotificationOpenedApp and getInitialNotification handlers; instead add runtime
validation inside the callbacks: check remoteMessage is truthy, then verify
remoteMessage.data is an object, iterate its keys and keep only entries whose
values are strings (or stringify non-primitive values if desired), build a safe
Record<string,string> and pass that to sendDeepLink; update the handlers
referenced (onNotificationOpenedApp, getInitialNotification, sendDeepLink) so
they never receive an unchecked assertion of remoteMessage.data.
| type Props = { | ||
| onOpenNotificationSettings: () => void; | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Props 타입 이름을 컴포넌트 명명 규칙에 맞게 변경하세요.
코딩 가이드라인에 따르면 Props 타입 이름은 {ComponentName}Props 패턴을 따라야 합니다. Props 대신 PushDisabledBannerProps를 사용해야 합니다.
♻️ 제안하는 수정
-type Props = {
+type PushDisabledBannerProps = {
onOpenNotificationSettings: () => void;
};
-function PushDisabledBanner({ onOpenNotificationSettings }: Props) {
+function PushDisabledBanner({ onOpenNotificationSettings }: PushDisabledBannerProps) {As per coding guidelines: "Props type names must follow pattern {ComponentName}Props"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/app/notification/_components/PushDisabledBanner.tsx` around
lines 3 - 5, Rename the generic Props type to follow the component naming
convention: change the type name from Props to PushDisabledBannerProps and
update all references (e.g., the component's props annotation and any
destructuring/usages) in PushDisabledBanner (the PushDisabledBanner.tsx file) to
use PushDisabledBannerProps so the component signature and any imports/exports
remain consistent.
Source: Coding guidelines
작업 요약
작업 세부 내용
FCM 푸시 알림 연동 (앱)
pushNotification.ts)firebase.json설정)DELETE /api/v1/fcm/tokens)딥링크 처리 (웹)
APP_REQ_DEEP_LINK메시지 수신 시 알림type/refId기반 라우팅알림 타입별 이동 경로
TOURNAMENT_JOINEDTOURNAMENT_ITEM_ADDEDITEM_PARSING_COMPLETEDITEM_PARSING_FAILEDkind필드에 따른 분기 처리 추가알림 히스토리 페이지
PushDisabledBanner표시/notification페이지로 이동API 프록시 (웹)
/api/v1/[...path])WebBridge 타입 정리 (
packages/core)pushNotification.ts)기타
@react-native-kakaov1 → v2.4.5 업데이트연관 이슈
closes #165
Summary by CodeRabbit
New Features
Refactor
Chores
Style