Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 19 additions & 12 deletions apps/client/public/firebase-messaging-sw.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
/* eslint-env serviceworker */
/* eslint-disable no-undef */

importScripts(
'./firebase_sdk/firebase-app-compat.js',
'./firebase_sdk/firebase-messaging-compat.js',
'./firebase-config.js'
);
Comment on lines +4 to +8
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

firebase-config 로딩 방식 점검 필요 (런타임 ReferenceError 위험).

importScripts('./firebase-config.js')는 글로벌 스코프에 firebaseConfig 변수가 정의되어 있어야 합니다. 현재 레포에는 src/firebase-config.ts만 보이며, 번들 산출물이 public/firebase-config.js로 생성되지 않으면 SW에서 firebaseConfig 참조 시 즉시 실패합니다. 또한 이 파일과 firebase_sdk/*가 SPA 캐치올 리라이트의 영향을 받지 않는지 확인이 필요합니다.

다음으로 존재/형식을 확인해 주세요(전역 변수 할당 여부까지 점검):

가능하면 public/firebase-config.js는 다음과 같이 전역에 주입되도록 유지하세요.

// apps/client/public/firebase-config.js
self.firebaseConfig = { /* ...env에서 주입된 값... */ };

🏁 Script executed:

#!/bin/bash
# public/firebase-config.js 유무 및 전역 변수 정의 확인
if fd -a '^firebase-config\.js$' apps/client/public >/dev/null; then
  echo "[OK] apps/client/public/firebase-config.js 존재"
  echo "---- 파일 앞부분 ----"
  head -n 20 apps/client/public/firebase-config.js || true
  echo "--------------------"
else
  echo "[ERROR] apps/client/public/firebase-config.js 없음"
fi

# SDK 파일 존재 확인
fd -a '^firebase-app-compat\.js$' apps/client/public/firebase_sdk
fd -a '^firebase-messaging-compat\.js$' apps/client/public/firebase_sdk

Length of output: 418


firebase-config.js 및 SDK 파일 경로와 전역 변수 정의를 확인하려면 아래 스크립트를 실행해 주세요.


🏁 Script executed:

#!/bin/bash
echo "=== firebase-config.js 위치 및 내용 확인 ==="
config_path=$(fd -t f 'firebase-config.js' | head -n1)
if [ -n "$config_path" ]; then
  echo "[FOUND] $config_path"
  echo "---- 상위 20줄 ----"
  head -n 20 "$config_path"
  echo "---- 'firebaseConfig' 검색 ----"
  grep -R "firebaseConfig" "$config_path" || echo "[WARN] 'firebaseConfig' 할당 미발견"
else
  echo "[ERROR] firebase-config.js 파일을 찾을 수 없음"
fi

echo -e "\n=== firebase-app-compat.js 위치 ==="
fd -t f 'firebase-app-compat.js' || echo "[ERROR] 해당 파일 없음"

echo -e "\n=== firebase-messaging-compat.js 위치 ==="
fd -t f 'firebase-messaging-compat.js' || echo "[ERROR] 해당 파일 없음"

echo -e "\n=== firebase_sdk 디렉터리 및 내부 파일 목록 확인 ==="
sdk_dir=$(fd -t d 'firebase_sdk' | head -n1)
if [ -n "$sdk_dir" ]; then
  echo "[FOUND DIR] $sdk_dir"
  echo "---- 내부 파일 ----"
  ls "$sdk_dir" || true
else
  echo "[ERROR] firebase_sdk 디렉터리를 찾을 수 없음"
fi

Length of output: 228


apps/client/public에 firebase-config.jsfirebase-sdk 파일이 없음
importScripts에서 참조하는 firebase-config.js, firebase-app-compat.js, firebase-messaging-compat.js 파일이 존재하지 않아 서비스 워커 로딩 시 ReferenceError가 발생합니다. 빌드 프로세스가 해당 파일들을 apps/client/public에 산출하도록 설정하거나, importScripts 경로를 올바르게 조정하세요.

🤖 Prompt for AI Agents
In apps/client/public/firebase-messaging-sw.js around lines 4 to 8, the
importScripts references firebase-config.js, firebase-app-compat.js, and
firebase-messaging-compat.js which are missing and cause a ReferenceError when
the service worker loads; fix by ensuring the build step outputs those files
into apps/client/public (or copy them into that directory), or update the
importScripts paths to point to the correct existing locations (e.g., adjust to
/static/, /assets/, or correct relative path) and verify the files are served by
the dev/prod server so the service worker can successfully import them.


self.addEventListener('install', function () {
self.skipWaiting();
});

self.addEventListener('activate', function () {
console.log('fcm sw activate..');
console.log('실행중..');
});

self.addEventListener('push', function (e) {
if (!e.data.json()) return;
const resultData = e.data.json().notification;
const notificationTitle = resultData.title;
firebase.initializeApp(firebaseConfig);
const messaging = firebase.messaging();

messaging.onBackgroundMessage((payload) => {
console.log('Received background message ', payload);

const notificationTitle = payload.notification?.title ?? '알림이 도착했어요!';
const notificationOptions = {
body: resultData.body,
body: payload.notification?.body,
};
console.log(resultData.title, {
body: resultData.body,
});
e.waitUntil(
self.registration.showNotification(notificationTitle, notificationOptions)
);
self.registration.showNotification(notificationTitle, notificationOptions);
});
Comment on lines +21 to 29
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

onBackgroundMessage는 “data‑only” 메시지에만 호출됩니다. click 핸들러와 data fallback을 추가하세요.

payload.notification 필드가 포함된 푸시는 브라우저가 자체 표시하고 SW 콜백이 호출되지 않는 경우가 많습니다. 커스텀 UI가 필요하면 서버에서 data‑only 형태로 전송하고, SW에서는 payload.data도 처리하세요. 또한 알림 클릭 시 목적지로 이동하도록 notificationclick 핸들러를 추가하세요.

 messaging.onBackgroundMessage((payload) => {
   console.log('Received background message ', payload);

-  const notificationTitle = payload.notification?.title ?? '알림이 도착했어요!';
-  const notificationOptions = {
-    body: payload.notification?.body,
-  };
-  self.registration.showNotification(notificationTitle, notificationOptions);
+  const titleFromData = payload.data && (payload.data.title || payload.data.notificationTitle);
+  const bodyFromData = payload.data && (payload.data.body || payload.data.notificationBody);
+  const notificationTitle =
+    payload.notification?.title ?? titleFromData ?? '알림이 도착했어요!';
+  const notificationOptions = {
+    body: payload.notification?.body ?? bodyFromData ?? '',
+    data: payload.data || {},
+    // 아이콘/배지 등 필요 시 추가
+    // icon: '/icons/icon-192.png',
+    // badge: '/icons/badge-72.png',
+  };
+  self.registration.showNotification(notificationTitle, notificationOptions);
 });
+
+// 알림 클릭 시 포커스/열기
+self.addEventListener('notificationclick', (event) => {
+  event.notification.close();
+  const url = event.notification?.data?.url || event.notification?.data?.click_action || '/';
+  event.waitUntil(
+    (async () => {
+      const allClients = await clients.matchAll({ type: 'window', includeUncontrolled: true });
+      const existing = allClients.find((c) => c.url.includes(url));
+      if (existing) return existing.focus();
+      return clients.openWindow(url);
+    })()
+  );
+});

16 changes: 13 additions & 3 deletions apps/client/src/shared/apis/setting/axiosInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,19 @@ apiRequest.interceptors.request.use(async (config) => {
const isNoAuth = noAuthNeeded.some((url) => config.url?.includes(url));

if (!isNoAuth) {
const token = localStorage.getItem('token');

if (!token || token === 'undefined' || token === 'null' || token === '') {
let token = localStorage.getItem('token');
const email = localStorage.getItem('email');
if (email) {
try {
token = await refreshToken(email);
} catch (err) {
console.error('요청 인터셉터에서 토큰 재발급 실패:', err);
localStorage.removeItem('token');
localStorage.removeItem('email');
window.location.href = '/onboarding';
throw err;
}
Comment on lines +40 to +51
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

매 요청마다 토큰을 재발급하고 있어 서버 부하 및 중복 호출 위험이 큽니다.

email 존재 시 무조건 refreshToken을 호출해 모든 요청이 재발급을 트리거합니다. 동시 다발 요청 시 토큰 재발급이 중복으로 발생하고, 응답 인터셉터의 재시도와 겹쳐 지연 및 불필요 트래픽이 생깁니다. 토큰이 없거나 만료(또는 만료 임박)한 경우에만 재발급하고, 단일 진행(single-flight)으로 동시 요청을 합치는 게 안전합니다.

아래처럼 만료 검사 + 단일 진행으로 수정 제안합니다(요청 인터셉터 부분만 diff). 헬퍼는 별도 코드 블록 참고.

   if (!isNoAuth) {
-    let token = localStorage.getItem('token');
-    const email = localStorage.getItem('email');
-    if (email) {
-      try {
-        token = await refreshToken(email);
-      } catch (err) {
-        console.error('요청 인터셉터에서 토큰 재발급 실패:', err);
-        localStorage.removeItem('token');
-        localStorage.removeItem('email');
-        window.location.href = '/onboarding';
-        throw err;
-      }
-    } else {
-      console.error('토큰이 없습니다. 온보딩을 먼저 완료해주세요.');
-      throw new Error('토큰이 없습니다. 온보딩을 먼저 완료해주세요.');
-    }
-
-    config.headers.Authorization = `Bearer ${token}`;
+    let token = localStorage.getItem('token');
+    if (!token || isTokenExpired(token)) {
+      const email = localStorage.getItem('email');
+      if (!email) {
+        console.error('토큰/이메일이 없습니다. 온보딩을 먼저 완료해주세요.');
+        localStorage.removeItem('token');
+        localStorage.removeItem('email');
+        window.location.href = '/onboarding';
+        throw new Error('인증 정보 없음');
+      }
+      try {
+        if (!refreshPromise) {
+          refreshPromise = refreshToken(email).finally(() => {
+            refreshPromise = null;
+          });
+        }
+        token = await refreshPromise;
+      } catch (err) {
+        console.error('요청 인터셉터에서 토큰 재발급 실패:', err);
+        localStorage.removeItem('token');
+        localStorage.removeItem('email');
+        window.location.href = '/onboarding';
+        throw err;
+      }
+    }
+    config.headers = config.headers ?? {};
+    (config.headers as any)['Authorization'] = `Bearer ${token}`;
   }

헬퍼/전역 보조 코드(파일 상단 아무 곳에 추가):

let refreshPromise: Promise<string> | null = null;

function isTokenExpired(token: string, skewSec = 60): boolean {
  try {
    const [, payload] = token.split('.');
    const data = JSON.parse(atob(payload));
    if (!data?.exp) return false;
    const now = Math.floor(Date.now() / 1000);
    return data.exp - now <= skewSec;
  } catch {
    // 파싱 실패 시 보수적으로 만료로 간주하여 재발급 유도
    return true;
  }
}
🤖 Prompt for AI Agents
In apps/client/src/shared/apis/setting/axiosInstance.ts around lines 40-51, the
interceptor unconditionally calls refreshToken for every request which causes
redundant refreshes and server load; change it to only call refreshToken when
there is no token or the token is expired/about-to-expire using an
isTokenExpired(token, skewSec) check, and implement a single-flight refresh
using a module-level refreshPromise: if a refresh is already in progress await
that promise instead of starting a new one, on success store the new token in
localStorage and use it for the request, and on failure clear token/email, reset
refreshPromise to null, redirect to onboarding and rethrow the error; ensure
refreshPromise is cleared on both success and failure so subsequent requests can
retry.

} else {
console.error('토큰이 없습니다. 온보딩을 먼저 완료해주세요.');
throw new Error('토큰이 없습니다. 온보딩을 먼저 완료해주세요.');
}
Expand Down
6 changes: 5 additions & 1 deletion apps/client/vercel.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
{
"rewrites": [
{
"source": "/firebase-messaging-sw.js",
"destination": "/firebase-messaging-sw.js"
},
{
"source": "/(.*)",
"destination": "/index.html"
}
]
}
}
22 changes: 17 additions & 5 deletions apps/extension/src/apis/axiosInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
);
const newToken = response.data.data.token;
chrome.storage.local.set({ token: newToken }, () => {
console.log('Token re-saved to chrome storage');

Check warning on line 19 in apps/extension/src/apis/axiosInstance.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
});
return newToken;
};
Expand All @@ -33,16 +33,28 @@

let token = await new Promise<string | undefined>((resolve) => {
chrome.storage.local.get('token', (result) => {
resolve(result.token);
resolve(result.token);
});
});

// 토큰 없으면 fetchToken 호출
if (!token || token === 'undefined') {
token = await fetchToken(email);
if (!isNoAuth) {
if (email) {
try {
token = await fetchToken(email);
} catch (err) {
console.error('요청 인터셉터에서 토큰 재발급 실패:', err);

Check warning on line 45 in apps/extension/src/apis/axiosInstance.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
localStorage.removeItem('token');
localStorage.removeItem('email');
window.location.href = '/onboarding';
throw err;
}
} else {
throw new Error('토큰이 없습니다. 온보딩을 먼저 완료해주세요.');
}

config.headers.Authorization = `Bearer ${token}`;
}

config.headers.Authorization = `Bearer ${token}`;
return config;
});

Expand Down
Loading