Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/fcm-click'
Browse files Browse the repository at this point in the history
  • Loading branch information
kanguk01 committed Nov 15, 2024
2 parents a6d9ef0 + 00dcaca commit b716111
Show file tree
Hide file tree
Showing 8 changed files with 441 additions and 199 deletions.
14 changes: 13 additions & 1 deletion public/firebase-messaging-sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,20 @@ messaging.onBackgroundMessage((payload) => {
const notificationTitle = payload.notification.title;
const notificationOptions = {
body: payload.notification.body,
icon: "/icon.png", // 아이콘 이미지 경로
icon: "./src/assets/icon.png", // 아이콘 이미지 경로
data: { click_action: payload.data.click_action } // 클릭 시 이동할 URL을 data에 추가
};

self.registration.showNotification(notificationTitle, notificationOptions);
});

// 알림 클릭 이벤트 리스너 추가
self.addEventListener("notificationclick", (event) => {
event.notification.close(); // 알림 닫기

// 알림에 설정된 URL로 이동
const targetUrl = event.notification.data.click_action || "https://splanet.co.kr/";
event.waitUntil(
clients.openWindow(targetUrl)
);
});
34 changes: 34 additions & 0 deletions src/api/hooks/useFcmOffsetUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useMutation } from "@tanstack/react-query";
import { apiClient } from "../instance";

interface FcmOffsetUpdateRequest {
token: string;
notificationOffset: number;
}

type FcmOffsetUpdateResponse = string;

const useFcmOffsetUpdate = () => {
return useMutation<FcmOffsetUpdateResponse, Error, FcmOffsetUpdateRequest>({
mutationFn: async (data: FcmOffsetUpdateRequest) => {
// query parameter로 변경
const response = await apiClient.put<FcmOffsetUpdateResponse>(
"/api/fcm/update/notification-offset",
null, // body는 null로 설정
{
params: {
// query parameters로 전송
token: data.token,
notificationOffset: data.notificationOffset,
},
},
);
return response.data;
},
onError: (error) => {
console.error("FCM 알림 오프셋 업데이트 실패:", error);
},
});
};

export default useFcmOffsetUpdate;
40 changes: 21 additions & 19 deletions src/api/hooks/useFcmUpdate.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,35 @@
import { useMutation } from "@tanstack/react-query";
import { apiClient } from "../instance";
import { apiClient } from "@/api/instance";

interface FcmUpdateRequest {
interface UpdateNotificationEnabledRequest {
token: string;
isNotificatioinEnabled?: boolean;
notificationOffset?: number;
isNotificationEnabled: boolean;
}

interface FcmUpdateResponse {
message: string;
}

const useFcmUpdate = () => {
return useMutation<FcmUpdateResponse, Error, FcmUpdateRequest>({
mutationFn: async (data: FcmUpdateRequest) => {
const response = await apiClient.put<FcmUpdateResponse>(
"/api/fcm/update",
export const useUpdateNotificationEnabled = () => {
return useMutation({
mutationFn: async ({
token,
isNotificationEnabled,
}: UpdateNotificationEnabledRequest) => {
const response = await apiClient.put(
`/api/fcm/update/notification-enabled?token=${token}&isNotificationEnabled=${isNotificationEnabled}`,
{}, // 빈 body
{
token: data.token,
isNotificationEnabled: data.isNotificatioinEnabled ?? true,
notificationOffset: data.notificationOffset ?? 0,
withCredentials: true,
},
);

return response.data;
},
onError: (error) => {
console.error("FCM 토큰 업데이트 실패:", error);
onError: (error: any) => {
if (error.response?.status === 401) {
console.error("인증 정보가 없거나 만료되었습니다.");
} else {
console.error("알림 설정 업데이트 실패:", error);
}
},
});
};

export default useFcmUpdate;
export default useUpdateNotificationEnabled;
Binary file added src/assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
108 changes: 108 additions & 0 deletions src/hooks/useNotificationSetup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { useEffect, useRef } from "react";
import { requestForToken, setupOnMessageListener } from "@/api/firebaseConfig";
import { apiClient } from "@/api/instance";

const FCM_TOKEN_KEY = "fcm_token";

// 모듈 레벨에서 전역 플래그 선언
let isTokenRequestedGlobal = false;

const useNotificationSetup = () => {
const tokenRequestedRef = useRef(false);

// 브라우저별 알림 설정 안내 메시지
const openNotificationSettings = () => {
const { userAgent } = navigator;
if (userAgent.includes("Edg")) {
alert(
"Edge 설정에서 알림을 활성화해주세요:\n설정 > 쿠키 및 사이트 권한 > 알림",
);
} else if (userAgent.includes("Chrome")) {
alert(
"Chrome 설정에서 알림을 활성화해주세요:\n설정 > 개인정보 및 보안 > 사이트 설정 > 알림",
);
} else if (
userAgent.includes("Safari") &&
!userAgent.includes("Chrome") &&
!userAgent.includes("Edg")
) {
alert(
"Safari 설정에서 알림을 활성화해주세요:\nmacOS에서는 Safari > 설정 > 알림\niOS에서는 설정 > Safari > 알림",
);
} else if (userAgent.includes("Firefox")) {
alert(
"Firefox 설정에서 알림을 활성화해주세요:\n설정 페이지에서 개인정보 및 보안 > 권한 > 알림",
);
} else {
alert("알림을 활성화하려면 브라우저 설정을 확인해주세요.");
}
};

useEffect(() => {
const initializeFCM = async () => {
// 알림 API 지원 여부 확인
if (!("Notification" in window)) {
alert("현재 사용 중인 브라우저는 알림 기능을 지원하지 않습니다.");
return;
}

// 이미 토큰 요청이 진행 중이거나 완료된 경우 중복 실행 방지
if (isTokenRequestedGlobal || tokenRequestedRef.current) {
return;
}

try {
isTokenRequestedGlobal = true; // 전역 플래그 설정
tokenRequestedRef.current = true;

const existingToken = localStorage.getItem(FCM_TOKEN_KEY);
if (!existingToken) {
if (Notification.permission === "denied") {
openNotificationSettings();
return;
}

const permission = await Notification.requestPermission();
if (permission === "granted") {
const fcmToken = await requestForToken();
if (fcmToken) {
await apiClient.post("/api/fcm/register", { token: fcmToken });
localStorage.setItem(FCM_TOKEN_KEY, fcmToken);
console.log("New FCM token registered and saved:", fcmToken);
}
} else if (permission === "denied") {
alert(
"알림 권한이 거부되었습니다. 브라우저 설정에서 알림을 활성화해주세요.",
);
openNotificationSettings();
}
}

// 메시지 리스너 설정
setupOnMessageListener();
} catch (error) {
console.error("Failed to initialize FCM:", error);
// 에러 발생 시 플래그 초기화
isTokenRequestedGlobal = false;
tokenRequestedRef.current = false;
}
};

initializeFCM();

return () => {
tokenRequestedRef.current = false;
};
}, []);

const getFCMToken = () => {
return localStorage.getItem(FCM_TOKEN_KEY);
};

return {
getFCMToken,
openNotificationSettings,
};
};

export default useNotificationSetup;
110 changes: 22 additions & 88 deletions src/pages/Main/MainPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import useDeletePlan from "@/api/hooks/useDeletePlan";
import Button from "@/components/common/Button/Button";
import Modal from "@/components/common/Modal/Modal";
import ReactDatePicker from "@/components/features/DatePicker/DatePicker";
import { requestForToken, setupOnMessageListener } from "@/api/firebaseConfig";
import { apiClient } from "@/api/instance";
import useUserData from "@/api/hooks/useUserData";
import useNotificationSetup from "@/hooks/useNotificationSetup";

const PageContainer = styled.div`
background-color: #ffffff;
Expand Down Expand Up @@ -83,6 +83,8 @@ const Spinner = styled.div`
`;

export default function MainPage() {
useNotificationSetup();

const location = useLocation();
const navigate = useNavigate();
const { data: fetchedPlans, isLoading, error, refetch } = useGetPlans();
Expand All @@ -99,87 +101,9 @@ export default function MainPage() {
const { mutateAsync: createPlan } = useCreatePlan();
const { mutateAsync: deletePlan } = useDeletePlan();
const { userData } = useUserData();
const isTokenRegistered = useRef(false);
const hasMounted = useRef(false);
const savePlanMutation = useCreatePlan();
const isPlanSaved = useRef(false);

// FCM 토큰 등록 함수
const registerFcmToken = async () => {
// 이미 토큰이 등록되어 있다면 종료
if (isTokenRegistered.current) {
console.log("이미 FCM 토큰이 등록되어 있습니다.");
return;
}

// localStorage에서 토큰 확인
const storedToken = localStorage.getItem("fcmToken");
if (storedToken) {
console.log(
"저장된 FCM 토큰을 사용합니다:",
`${storedToken.slice(0, 10)}...`,
);
isTokenRegistered.current = true;
return;
}

try {
console.log("FCM 토큰 등록 시작...");
const permission = await Notification.requestPermission();
console.log("알림 권한 상태:", permission);

if (permission === "granted") {
const fcmToken = await requestForToken();
if (fcmToken) {
console.log("새로운 FCM 토큰 발급됨:", `${fcmToken.slice(0, 10)}...`);
await apiClient.post("/api/fcm/register", { token: fcmToken });
localStorage.setItem("fcmToken", fcmToken);
isTokenRegistered.current = true;
console.log("FCM 토큰 등록 완료");
} else {
console.warn("FCM 토큰이 null입니다.");
}
} else {
console.warn("알림 권한이 거부되었습니다.");
}
} catch (err) {
console.error("FCM 토큰 등록 중 오류 발생:", err);
}
};
// Notification functionality (기존 코드 유지)
useEffect(() => {
const registerFcmToken = async () => {
const permission = await Notification.requestPermission();
if (permission === "granted") {
try {
const fcmToken = await requestForToken();
if (fcmToken) {
await apiClient.post("/api/fcm/register", { token: fcmToken });
console.log("FCM 토큰이 성공적으로 등록되었습니다.");
}
} catch (err) {
console.error("FCM 토큰 등록 중 오류 발생:", err);
}
} else {
console.log("알림 권한이 거부되었습니다.");
}
};

registerFcmToken();
setupOnMessageListener(); // Set up the listener for foreground messages
}, []);
// 앱 초기 마운트시에만 FCM 토큰 등록 및 리스너 설정
useEffect(() => {
if (!hasMounted.current) {
console.log("FCM 초기화 시작...");
registerFcmToken().then(() => {
console.log("FCM 초기화 완료");
setupOnMessageListener();
});
hasMounted.current = true;
}
}, []);

// 플랜 데이터 초기화
useEffect(() => {
if (fetchedPlans) {
Expand Down Expand Up @@ -282,8 +206,8 @@ export default function MainPage() {
accessibility: true,
isCompleted: false,
});
} catch (error) {
alert(`추가 중 오류 발생: ${error}`);
} catch (err) {
alert(`추가 중 오류 발생: ${err}`);
}
};

Expand All @@ -296,8 +220,8 @@ export default function MainPage() {
setModifiedPlans((prevPlans) =>
prevPlans.filter((plan) => plan.id !== planId),
);
} catch (error) {
alert(`삭제 중 오류 발생: ${error}`);
} catch (err) {
alert(`삭제 중 오류 발생: ${err}`);
}
}
};
Expand All @@ -324,8 +248,8 @@ export default function MainPage() {
// 상태 업데이트
setModifiedPlans(updatedPlans);

// 변경된 플랜들에 대해 서버에 업데이트
for (const plan of changedPlans) {
// Promise.all을 사용하여 모든 플랜을 병렬로 업데이트
const updatePlans = changedPlans.map(async (plan) => {
try {
await apiClient.put(`/api/plans/${plan.id}`, {
title: plan.title,
Expand All @@ -336,13 +260,23 @@ export default function MainPage() {
isCompleted: plan.isCompleted ?? false,
});
console.log(`플랜 ID ${plan.id}이 성공적으로 업데이트되었습니다.`);
} catch (error: unknown) {
if (error instanceof Error) {
alert(`플랜 업데이트 중 오류 발생: ${error.message}`);
} catch (err) {
if (err instanceof Error) {
alert(`플랜 업데이트 중 오류 발생: ${err.message}`);
} else {
alert("플랜 업데이트 중 알 수 없는 오류가 발생했습니다.");
}
// 필요시 개별 에러를 처리하거나 다시 던질 수 있습니다.
throw err;
}
});

try {
await Promise.all(updatePlans);
console.log("모든 플랜이 성공적으로 업데이트되었습니다.");
} catch (err) {
console.error("일부 플랜 업데이트에 실패했습니다.", error);
// 추가적인 에러 핸들링 로직을 여기에 작성할 수 있습니다.
}
},
[modifiedPlans],
Expand Down
Loading

0 comments on commit b716111

Please sign in to comment.