Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat]: 웹 푸시 알림 기능 구현 #59

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ dist-ssr
*.sln
*.sw?

*storybook.log
*storybook.log

.env
27 changes: 27 additions & 0 deletions firebase-messaging-sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// firebase-messaging-sw.js
importScripts('https://www.gstatic.com/firebasejs/9.0.0/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/9.0.0/firebase-messaging-compat.js');

firebase.initializeApp({
apiKey: "AIzaSyAInigygScRLDilnWcnArBN8LMbQRpDZVk",
authDomain: "splanet-cef14.firebaseapp.com",
projectId: "splanet-cef14",
storageBucket: "splanet-cef14.appspot.com",
messagingSenderId: "995362943401",
appId: "1:995362943401:web:cef434d0e3f51d31a4d4b8",
measurementId: "G-LZJKRYBSJV"
});

const messaging = firebase.messaging();

// 알림 수신 대기
messaging.onBackgroundMessage((payload) => {
console.log('Received background message ', payload);
// Customize notification
const notificationTitle = payload.notification.title;
const notificationOptions = {
body: payload.notification.body,
};

self.registration.showNotification(notificationTitle, notificationOptions);
});
29 changes: 29 additions & 0 deletions public/firebase-messaging-sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// public/firebase-messaging-sw.js

importScripts('https://www.gstatic.com/firebasejs/9.0.0/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/9.0.0/firebase-messaging-compat.js');

// 환경 변수에서 Firebase 설정값 가져오기
firebase.initializeApp({
apiKey: "AIzaSyAInigygScRLDilnWcnArBN8LMbQRpDZVk",
authDomain: "splanet-cef14.firebaseapp.com",
projectId: "splanet-cef14",
storageBucket: "splanet-cef14.appspot.com",
messagingSenderId: "995362943401",
appId: "1:995362943401:web:cef434d0e3f51d31a4d4b8",
measurementId: "G-LZJKRYBSJV"
});

const messaging = firebase.messaging();

// 백그라운드 메시지 처리
messaging.onBackgroundMessage((payload) => {
console.log('Received background message ', payload);
const notificationTitle = payload.notification.title;
const notificationOptions = {
body: payload.notification.body,
icon: '/icon.png' // 아이콘 이미지 경로
};

self.registration.showNotification(notificationTitle, notificationOptions);
});
Binary file added public/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 56 additions & 0 deletions src/api/firebaseConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// src/api/firebaseConfig.ts

import { initializeApp } from 'firebase/app';
import { getMessaging, getToken, onMessage as firebaseOnMessage } from 'firebase/messaging';

// 환경 변수에서 Firebase 설정값 가져오기
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
appId: import.meta.env.VITE_FIREBASE_APP_ID,
measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID
};

// Firebase 초기화
const app = initializeApp(firebaseConfig);
const messaging = getMessaging(app);

// VAPID 키 가져오기
const vapidKey = import.meta.env.VITE_FIREBASE_VAPID_KEY;

// FCM 토큰 요청 함수
export const requestForToken = async () => {
try {
const currentToken = await getToken(messaging, { vapidKey });
if (currentToken) {
console.log('FCM token:', currentToken);
return currentToken;
} else {
console.log('No registration token available. Request permission to generate one.');
return null;
}
} catch (error) {
console.error('An error occurred while retrieving token. ', error);
return null;
}
};

// 메시지 수신 리스너 설정
export const setupOnMessageListener = () => {
firebaseOnMessage(messaging, (payload) => {
console.log("Message received: ", payload);

const notificationTitle = payload.notification?.title || "알림";
const notificationOptions = {
body: payload.notification?.body || "새로운 알림이 도착했습니다.",
};

// 브라우저 알림 표시
new Notification(notificationTitle, notificationOptions);
});
};

export { messaging };
10 changes: 10 additions & 0 deletions src/components/common/ProfileImage/ProfileImage.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import styled from "@emotion/styled";

const StyledImage = styled.img`
border-radius: 50%;
object-fit: cover;
width: 60px;
height: 60px;
`;

export default StyledImage;
53 changes: 40 additions & 13 deletions src/pages/Main/MainPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import { useGetPlans } from "@/api/hooks/useGetPlans";
import useCreatePlan from "@/api/hooks/useCreatePlans";
import CircleButton from "@/components/common/CircleButton/CircleButton";
import breakpoints from "@/variants/breakpoints";
import { requestForToken, setupOnMessageListener } from "@/api/firebaseConfig";
import { apiClient } from "@/api/instance";
import useAuth from "@/hooks/useAuth"; // auth 상태 가져오기 위한 훅

// 캘린더와 버튼을 포함하는 반응형 컨테이너
const CalendarContainer = styled.div`
position: relative;
width: 100%;
max-width: 1200px; // 최대 너비를 설정해 버튼이 중앙을 유지하도록 합니다.
max-width: 1200px;
margin: 0 auto;
padding: 20px;
`;
Expand Down Expand Up @@ -52,14 +55,14 @@ const ModalOverlay = styled.div`

const ModalContent = styled.div`
background: white;
padding: 20px 30px 20;
width: 500px; // 가로폭 설정
padding: 20px 30px;
width: 500px;
max-width: 95%;
border-radius: 8px;
box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
gap: 15px; /* 각 입력 필드 사이에 간격 추가 */
gap: 15px;
`;

const Input = styled.input`
Expand Down Expand Up @@ -135,6 +138,7 @@ const ToggleLabel = styled.span`

const MainPage: React.FC = () => {
const { data: plans, isLoading, error } = useGetPlans();
const { authState } = useAuth(); // auth 상태를 가져오기 위한 훅
const [modalOpen, setIsModalOpen] = useState(false);
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
Expand Down Expand Up @@ -166,23 +170,46 @@ const MainPage: React.FC = () => {
accessibility: isAccessible,
isCompleted,
});
setIsModalOpen(false); // 폼 제출 후 모달 닫기
setIsModalOpen(false);
};

useEffect(() => {
// 로컬 스토리지 정리 (필요한 경우)
const savedPreviewData = localStorage.getItem("previewPlanData");
if (savedPreviewData) {
localStorage.removeItem("previewPlanData");
const registerFcmToken = async () => {
// 알림 권한 요청
const permission = await Notification.requestPermission();
if (permission === 'granted') {
try {
// FCM 토큰 요청
const fcmToken = await requestForToken();
if (fcmToken && authState.isAuthenticated) {
// FCM 토큰을 백엔드에 등록
await apiClient.post(
'/api/fcm/register',
{ token: fcmToken }
);
console.log('FCM 토큰이 성공적으로 등록되었습니다.');
}
} catch (error) {
console.error('FCM 토큰 등록 중 오류 발생:', error);
}
} else {
console.log('알림 권한이 거부되었습니다.');
}
};

// authState.isAuthenticated가 true일 때만 FCM 토큰을 등록
if (authState.isAuthenticated) {
registerFcmToken();
setupOnMessageListener(); // 포그라운드 메시지 수신 리스너 설정
}
}, []);
}, [authState.isAuthenticated]);

if (isLoading) {
return <div>Loading...</div>; // 로딩 상태 처리
return <div>Loading...</div>;
}

if (error) {
return <div>Error: {error.message}</div>; // 에러 처리
return <div>Error: {error.message}</div>;
}

return (
Expand Down Expand Up @@ -245,7 +272,6 @@ const MainPage: React.FC = () => {
/>
</ToggleWrapper>

{/* 완료 여부 토글 */}
<ToggleWrapper>
<ToggleLabel>완료 여부</ToggleLabel>
<ToggleInput
Expand All @@ -263,4 +289,5 @@ const MainPage: React.FC = () => {
</CalendarContainer>
);
};

export default MainPage;