From 4bf2b61790cc782e8c66798f0d68bff7a461f035 Mon Sep 17 00:00:00 2001 From: Jun279 <33.beautifulboy@gmail.com> Date: Mon, 11 May 2026 17:55:43 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=EB=B8=8C=EB=9D=BC=EC=9A=B0=EC=A0=80=20?= =?UTF-8?q?=ED=91=B8=EC=8B=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/sw.js | 11 ++++++++ src/apis/apiHooks/usePushNotification.ts | 28 ++++++++++++++++++++ src/apis/notification/notification.ts | 15 +++++++++++ src/main.tsx | 6 +++++ src/pages/login/components/OAuthCallback.tsx | 3 +++ src/types/notification/notification.type.ts | 13 +++++++++ 6 files changed, 76 insertions(+) create mode 100644 public/sw.js create mode 100644 src/apis/apiHooks/usePushNotification.ts create mode 100644 src/apis/notification/notification.ts create mode 100644 src/types/notification/notification.type.ts diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..75126e2 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,11 @@ +// Service Worker +self.addEventListener('push', event => { + const data = event.data?.json(); + + event.waitUntil( + self.registration.showNotification(data?.title ?? '알림', { + body: data?.body ?? '', + icon: '/icon.png', + }) + ); +}); diff --git a/src/apis/apiHooks/usePushNotification.ts b/src/apis/apiHooks/usePushNotification.ts new file mode 100644 index 0000000..c41cf16 --- /dev/null +++ b/src/apis/apiHooks/usePushNotification.ts @@ -0,0 +1,28 @@ +import { getVapidPublicKey, postPushSubscription } from "@/apis/notification/notification"; + +export const usePushNotification = () => { + const subscribePushNotification = async () => { + try { + const vapidPublicKeyResponse = await getVapidPublicKey(); + const vapidPublicKey = vapidPublicKeyResponse.data.publicKey; + + const registration = await navigator.serviceWorker.ready; // 서비스 워커 가져오기(대기) + const subscription = await registration.pushManager.subscribe({ // 푸시 구독 요청 + userVisibleOnly: true, + applicationServerKey: vapidPublicKey, + }); + + await postPushSubscription({ + endpoint: subscription.endpoint, + keys: { + p256dh: btoa(String.fromCharCode(...new Uint8Array(subscription.getKey('p256dh')!))), + auth: btoa(String.fromCharCode(...new Uint8Array(subscription.getKey('auth')!))), + } + }); + } catch (error) { + console.error('푸시 구독 실패: ', error); + } + }; + + return {subscribePushNotification}; +}; diff --git a/src/apis/notification/notification.ts b/src/apis/notification/notification.ts new file mode 100644 index 0000000..7a4ec60 --- /dev/null +++ b/src/apis/notification/notification.ts @@ -0,0 +1,15 @@ +import { instance } from '@/apis/instance'; +import type { ApiEnvelope } from '@/types/api.type'; +import type { GetVapidPublicKeyResponse, PostPushSubscriptionRequest } from '@/types/notification/notification.type'; + +// 알림 구독 키 조회 +export const getVapidPublicKey = async (): Promise> => { + const response = await instance.get('/notifications/web-push/public-key'); + return response.data; +}; + +// 알림 구독 +export const postPushSubscription = async (body: PostPushSubscriptionRequest): Promise> => { + const response = await instance.post('/notifications/web-push/subscriptions', body); + return response.data; +}; diff --git a/src/main.tsx b/src/main.tsx index 6a4a355..e8b2916 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,6 +6,12 @@ import App from './App'; const queryClient = new QueryClient(); +if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/sw.js') + .then(() => console.log('Service Worker 등록 성공')) + .catch(error => console.error('Service Worker 등록 실패: ', error)); +} + createRoot(document.getElementById('root')!).render( diff --git a/src/pages/login/components/OAuthCallback.tsx b/src/pages/login/components/OAuthCallback.tsx index d7da184..998d83f 100644 --- a/src/pages/login/components/OAuthCallback.tsx +++ b/src/pages/login/components/OAuthCallback.tsx @@ -1,11 +1,13 @@ import { useEffect } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { useAuthStore } from '@/stores/authStore'; +import { usePushNotification } from '@/apis/apiHooks/usePushNotification'; const OAuthCallback = () => { const [searchParams] = useSearchParams(); const { login } = useAuthStore(); const navigate = useNavigate(); + const { subscribePushNotification } = usePushNotification(); useEffect(() => { const accessToken = searchParams.get('accessToken'); @@ -17,6 +19,7 @@ const OAuthCallback = () => { } login(accessToken); + subscribePushNotification(); // 초대 링크로 왔었으면 const inviteCode = sessionStorage.getItem('inviteCode'); diff --git a/src/types/notification/notification.type.ts b/src/types/notification/notification.type.ts new file mode 100644 index 0000000..82deb3f --- /dev/null +++ b/src/types/notification/notification.type.ts @@ -0,0 +1,13 @@ +// 알림 구독 키 조회 +export interface GetVapidPublicKeyResponse { + publicKey: string; +}; + +// 알림 구독 +export interface PostPushSubscriptionRequest { + endpoint: string; + keys: { + p256dh: string; + auth: string; + }; +};