Skip to content

feat: FCM 푸시 알림 연동 및 딥링크 처리 - #166

Merged
soyeong0115 merged 27 commits into
devfrom
feat/165-fcm-push-notification
Jun 10, 2026
Merged

feat: FCM 푸시 알림 연동 및 딥링크 처리#166
soyeong0115 merged 27 commits into
devfrom
feat/165-fcm-push-notification

Conversation

@kanghaeun

@kanghaeun kanghaeun commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

작업 요약

  • FCM 푸시 알림 권한 요청·토큰 관리·딥링크 라우팅 연동 및 WebBridge 타입 정리

작업 세부 내용

FCM 푸시 알림 연동 (앱)

  • FCM 권한 요청 및 토큰 발급/갱신 처리 (pushNotification.ts)
  • 백그라운드·종료 상태 알림 수신 및 탭 시 딥링크 분기 처리
  • 포그라운드 상태에서는 OS 배너 미표시 (firebase.json 설정)
  • 로그인 후 FCM 토큰 서버 등록, 로그아웃 시 토큰 해제 (DELETE /api/v1/fcm/tokens)
  • 로그인 성공 후 WebView 쿠키 즉시 동기화

딥링크 처리 (웹)

  • APP_REQ_DEEP_LINK 메시지 수신 시 알림 type / refId 기반 라우팅

  • 알림 타입별 이동 경로

    알림 타입 이동 경로
    TOURNAMENT_JOINED 토너먼트 생성 페이지
    TOURNAMENT_ITEM_ADDED 토너먼트 생성 페이지
    ITEM_PARSING_COMPLETED 아카이브 또는 토너먼트 페이지
    ITEM_PARSING_FAILED 아카이브 또는 토너먼트 페이지
  • kind 필드에 따른 분기 처리 추가

알림 히스토리 페이지

  • 알림 권한 거부 시 PushDisabledBanner 표시
  • 헤더 알림 아이콘 클릭 시 /notification 페이지로 이동

API 프록시 (웹)

  • 실기기 테스트 시 CORS 문제 해결을 위한 Next.js API Route 프록시 추가 (/api/v1/[...path])
  • Origin 헤더 제거 후 백엔드로 포워딩

WebBridge 타입 정리 (packages/core)

  • 푸시 알림 관련 타입/상수 추가 (pushNotification.ts)
  • 송/수신 대상 명확하게 구분하도록 WebBridge 메시지 타입 정리

기타

  • @react-native-kakao v1 → v2.4.5 업데이트
  • 앱 아이콘 이미지 업데이트
  • Firebase config 파일 EAS Secret으로 분리

연관 이슈

closes #165

Summary by CodeRabbit

  • New Features

    • 앱-웹 푸시 권한/FCM 토큰 초기화·동기화, 백그라운드 메시지 처리 및 딥링크 라우팅 추가
    • 알림 히스토리 UI와 알림 권한 설정 열기 기능 추가
    • 서버로 FCM 토큰 전송/삭제 API 및 프록시 API 라우트 추가
  • Refactor

    • 웹↔네이티브 메시지 규격을 WEB_REQ_/APP_RES_ 형식으로 통일하고 호출 형식 표준화
  • Chores

    • Firebase·Kakao·Expo 관련 의존성 정리 및 구성 업데이트
  • Style

    • 개발 환경용 파일 무시 설정에 서비스 설정 파일 추가

iOdiO89 and others added 22 commits June 7, 2026 01:12
- 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 재요청
@vercel

vercel Bot commented Jun 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
depromeet Ready Ready Preview, Comment Jun 10, 2026 7:12am

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@soyeong0115, we couldn't start this review because you've reached your PR review rate limit.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e1d475c5-36f2-4861-b3c7-aff066f4b64d

📥 Commits

Reviewing files that changed from the base of the PR and between 58f3499 and 7987cf5.

📒 Files selected for processing (1)
  • apps/web/src/app/login/_components/LoginButtons.tsx

Walkthrough

앱·웹 WebBridge 규약을 WEB_REQ_/APP_RES_로 전환하고, 앱에 FCM 권한·토큰·리스너를 추가해 웹뷰로 상태·토큰을 전송하며 웹에서 토큰 등록·딥링크 라우팅·알림 UI를 연동합니다.

Changes

푸시 연동 및 브리지 프로토콜 전환

Layer / File(s) Summary
Core 계약 및 타입 재정의
packages/core/src/consts/webBridge.ts, packages/core/src/types/*, packages/core/src/index.ts
WebBridge 메시지 타입과 Ready/이미지/푸시/공유 타입을 WEB_REQ_/APP_RES_ 규칙으로 재정의하고 public export를 갱신합니다.
앱 빌드·런타임 설정
apps/app/app.config.ts, apps/app/app.json, apps/app/.gitignore, apps/app/package.json, apps/app/firebase.json
Expo 설정을 환경변수로 주입하고 iOS/Android 권한·entitlements·플러그인·의존성을 추가하며 Firebase 서비스 파일을 .gitignore에 추가합니다.
앱 FCM 권한·토큰 서비스
apps/app/utils/pushNotification.ts, apps/app/index.js
플랫폼별 권한 체크/요청, FCM 토큰 조회·동기화, 토큰 갱신/포그라운드/오픈/초기/백그라운드 메시지 리스너를 구현합니다.
앱 엔트리·Provider 연결
apps/app/components/PushNotificationProvider.tsx, apps/app/app/_layout.tsx
앱 시작 시 FCM 초기화와 메시징 리스너 등록, AppState 변경 시 웹 동기화를 수행하는 Provider를 루트에 마운트합니다.
앱 메시지 포맷·쿠키 동기화 변경
apps/app/utils/webBridge.ts, apps/app/hooks/*, apps/app/utils/handleImage.ts
WebBridge.postMessage를 메시지 객체 단일 인자로 변경하고 injectCookies를 추가, 공유·소셜 로그인·이미지 피커 훅에서 새 메시지 규약을 사용하도록 갱신합니다.

웹: 토큰 API·프록시·알림 UI

Layer / File(s) Summary
Next API 프록시 및 설정
apps/web/src/app/api/v1/[...path]/route.ts, apps/web/next.config.js
모든 /api/v1/* 요청을 백엔드로 프록시하는 catch-all 라우트를 추가하고 rewrites를 제거, 개발용 허용 오리진 설정을 추가합니다.
FCM 토큰 등록/삭제 API & 로그아웃 연동
apps/web/src/apis/postFcmToken.ts, apps/web/src/apis/deleteFcmToken.ts, apps/web/src/app/_actions/logout.ts, apps/web/src/consts/api.ts
postFcmToken/deleteFcmToken를 추가하고 로그아웃 시 device_id 기반 토큰 삭제를 시도하며 엔드포인트 상수를 확장합니다.
웹 푸시 권한·토큰 동기화 훅
apps/web/src/app/notification/_hooks/usePushPermission.ts, apps/web/src/hooks/useFcmTokenSync.ts, apps/web/src/hooks/useDeepLink.ts, apps/web/src/utils/pushNotificationRoute.ts
웹뷰에서 권한 상태를 요청·수신하고, 수신한 FCM 토큰을 서버에 등록하며 앱에서 보낸 딥링크 요청을 라우팅합니다.
웹 알림 페이지 및 컴포넌트
apps/web/src/app/notification/*, apps/web/src/components/Providers.tsx, apps/web/src/components/header/index.tsx, apps/web/src/consts/route.ts
NotificationContent/EmptyState/PushDisabledBanner로 UI를 구성하고 Providers에 FcmTokenSyncer·DeepLinkHandler를 마운트하며 헤더 아이콘 경로를 /notification으로 변경합니다.
웹 로그인·이미지 브리지 소비부
apps/web/src/app/login/*, apps/web/src/hooks/useImagePicker.ts, apps/web/src/app/archive/*
웹 로그인에서 RN WebView.postMessage(JSON)로 브리지 요청을 전송하고 이미지 피커/공유 준비 메시지 규약을 새 타입으로 맞춥니다.

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)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • FCM 푸시 알림 기능 core#236 — 서버 측 /api/v1/fcm/tokens 엔드포인트 구현과 클라이언트의 post/delete FCM 토큰 흐름이 직접 연관됩니다.

Possibly related PRs

Suggested reviewers

  • soyeong0115
  • iOdiO89

Poem

"나는 토끼, 귀로 푹 듣네, 푸시가 달려오는 소식—
작은 토큰 하나로 웹과 앱이 손을 잡고,
딥링크 따라 길을 찾아가네.
당근 한 입보다 반가운 알림, 뛰어가 확인할래! 🥕🐇"

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning 카카오 라이브러리 버전 업데이트, 로그인 UI 변경 등 일부 이슈 범위 외 변경사항이 포함되어 있습니다. LoginButtons.tsx의 카카오/구글 로그인 리팩토링과 @react-native-kakao 버전 업그레이드가 FCM 기능 구현과 직접적인 관련이 없으므로 검토 후 별도 PR로 분리 고려를 권장합니다.
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 FCM 푸시 알림 연동 및 딥링크 처리라는 주요 변경사항을 명확하고 간결하게 요약하고 있습니다.
Linked Issues check ✅ Passed 모든 연결된 이슈 #165의 요구사항이 충족되었습니다. FCM 권한/토큰 관리, 백그라운드 알림 수신, 딥링크 라우팅, 토큰 서버 등록/해제, 알림 히스토리 페이지 등이 구현되었습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/165-fcm-push-notification

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.tsx Line 82-93은 isSyncedtrue일 때만 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 처리 재검토

usePushPermissionisPushEnableduseState<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 win

WebBridge 유틸리티를 사용하여 일관성과 타입 안전성을 개선하세요.

handleKakaoLoginhandleGoogleLoginwindow.ReactNativeWebView에 직접 접근하여 메시지를 전송하고 있습니다. 이는 다음 문제를 야기합니다:

  1. apps/web/src/utils/webBridge.tsWebBridge.postMessage 유틸리티가 제공하는 타입 가드(isRNWebViewWindow)를 우회합니다.
  2. 인라인 타입 단언이 중복되어 있습니다(19줄, 32줄).
  3. 다른 파일(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 value

Delete 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.log allowed; only warn and error are 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1e148eb and 1f2cea9.

⛔ Files ignored due to path filters (2)
  • apps/app/assets/images/icon.png is excluded by !**/*.png
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (48)
  • apps/app/.gitignore
  • apps/app/app.config.ts
  • apps/app/app.json
  • apps/app/app/_layout.tsx
  • apps/app/app/index.tsx
  • apps/app/components/PushNotificationProvider.tsx
  • apps/app/firebase.json
  • apps/app/hooks/useShareIntent.ts
  • apps/app/hooks/useSocialLogin.ts
  • apps/app/hooks/useWebBridgeMessage.ts
  • apps/app/hooks/useWebviewCookieSync.ts
  • apps/app/index.js
  • apps/app/package.json
  • apps/app/utils/handleImage.ts
  • apps/app/utils/pushNotification.ts
  • apps/app/utils/webBridge.ts
  • apps/web/next.config.js
  • apps/web/src/apis/client.ts
  • apps/web/src/apis/deleteFcmToken.ts
  • apps/web/src/apis/postFcmToken.ts
  • apps/web/src/app/_actions/logout.ts
  • apps/web/src/app/api/v1/[...path]/route.ts
  • apps/web/src/app/archive/_components/wish-grid/index.tsx
  • apps/web/src/app/archive/_hooks/useShareIntentWish.ts
  • apps/web/src/app/layout.tsx
  • apps/web/src/app/login/_components/LoginButtons.tsx
  • apps/web/src/app/login/_hooks/usePostGuestLogin.ts
  • apps/web/src/app/notification/_components/NotificationContent.tsx
  • apps/web/src/app/notification/_components/NotificationEmptyState.tsx
  • apps/web/src/app/notification/_components/PushDisabledBanner.tsx
  • apps/web/src/app/notification/_hooks/usePushPermission.ts
  • apps/web/src/app/notification/page.tsx
  • apps/web/src/components/Providers.tsx
  • apps/web/src/components/header/index.tsx
  • apps/web/src/consts/api.ts
  • apps/web/src/consts/route.ts
  • apps/web/src/hooks/useDeepLink.ts
  • apps/web/src/hooks/useFcmTokenSync.ts
  • apps/web/src/hooks/useImagePicker.ts
  • apps/web/src/hooks/useNativeLoginResult.ts
  • apps/web/src/utils/pushNotificationRoute.ts
  • apps/web/src/utils/webBridge.ts
  • packages/core/src/consts/webBridge.ts
  • packages/core/src/index.ts
  • packages/core/src/types/image.ts
  • packages/core/src/types/pushNotification.ts
  • packages/core/src/types/shareIntent.ts
  • packages/core/src/types/webBridge.ts
💤 Files with no reviewable changes (2)
  • apps/web/next.config.js
  • apps/app/hooks/useWebBridgeMessage.ts

Comment thread apps/app/app.json
"supportsTablet": false,
"bundleIdentifier": "day.no30s.piki",
"entitlements": {
"aps-environment": "development", // NOTE: 배포할때 'production'으로 변경

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

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

Comment thread apps/app/app/index.tsx
Comment on lines 40 to +66
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

브리지 프로토콜을 전면 교체했는데 호환 계층이 없습니다.

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.

Comment on lines +135 to +141
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,
});
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

refId 숫자 변환 시 유효성 검증이 필요합니다.

Line 139에서 data.refIdNumber()로 변환하지만, 값이 유효한 숫자 문자열인지 검증하지 않습니다. 잘못된 형식의 데이터가 들어올 경우 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.

Comment on lines +143 to +149
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);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

FCM remoteMessage.data 타입 안전성을 개선하세요.

Line 144와 148에서 remoteMessage.dataRecord<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.

Comment thread apps/web/src/app/api/v1/[...path]/route.ts
Comment thread apps/web/src/app/login/_hooks/usePostGuestLogin.ts Outdated
Comment on lines +3 to +5
type Props = {
onOpenNotificationSettings: () => void;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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

Comment thread apps/web/src/consts/api.ts Outdated
@kanghaeun
kanghaeun requested review from iOdiO89 and soyeong0115 June 10, 2026 00:56
@kanghaeun kanghaeun added the feature New feature or request label Jun 10, 2026
@kanghaeun kanghaeun added APP Good for newcomers WEB labels Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

APP Good for newcomers feature New feature or request WEB

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: FCM 푸시 알림 연동

3 participants