From d106f80516489f796f7e61f90a69e871d21366c2 Mon Sep 17 00:00:00 2001 From: ptyoiy <56474564+ptyoiy@users.noreply.github.com> Date: Wed, 3 Jul 2024 17:01:50 +0900 Subject: [PATCH] feat: add notification click event, token refresh feature --- public/firebaseSW.js | 72 +++++----- src/App.tsx | 84 ++++++++++-- src/api/axios/alarm.ts | 10 +- src/api/axios/index.ts | 1 + src/api/query/alarm.ts | 2 +- src/api/query/index.ts | 1 - src/api/query/user.ts | 43 ++---- src/components/Login/index.tsx | 4 +- src/components/Modal/MainModal/index.tsx | 13 +- .../PaperList/AlarmSettingList/index.tsx | 25 ---- src/context/index.tsx | 95 ++++++++++++- src/context/setting.ts | 129 ------------------ src/context/user.d.ts | 8 +- src/pages/MainPage/EventCard/index.tsx | 8 +- src/pages/MyPage/index.tsx | 106 +++++--------- src/pages/TerracePage/index.tsx | 60 +------- src/util/firebaseCloudMessage.ts | 23 +--- 17 files changed, 273 insertions(+), 411 deletions(-) delete mode 100644 src/components/PaperList/AlarmSettingList/index.tsx delete mode 100644 src/context/setting.ts diff --git a/public/firebaseSW.js b/public/firebaseSW.js index a961e10..3d12fe0 100644 --- a/public/firebaseSW.js +++ b/public/firebaseSW.js @@ -1,5 +1,6 @@ importScripts('https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js'); importScripts('https://www.gstatic.com/firebasejs/8.10.1/firebase-messaging.js'); + const firebaseConfig = { apiKey: 'AIzaSyAEU-CHEmYMLlSLnRLpmoABONwqHZGVREw', authDomain: 'test-csey.firebaseapp.com', @@ -12,47 +13,42 @@ const firebaseConfig = { const app = firebase.initializeApp(firebaseConfig); const messaging = firebase.messaging(app); -console.log('#@#object'); - -self.addEventListener('push', (event) => { - console.log("Push"); - const options = { - body: event.data.text(), - icon: 'icon.png', - badge: 'badge.png', - }; - event.waitUntil(self.registration.showNotification('Hi-Notification', options)); -}); - -// messaging -// .getToken({ -// vapidKey: -// 'BBHaXtLh_pZUn89NG0Jp1J_1N0wGS2R_9vllG20dfYsJ4dF5ZmTDofKD0HbBhoeZYhngL3YQ0GHUxRnnXpAkoko', -// }) -// .then((currentToken) => { -// if (currentToken) { -// // Send the token to your server and update the UI if necessary -// // ... -// console.log({ currentToken }); -// } else { -// // Show permission request UI -// console.log('No registration token available. Request permission to generate one.'); -// // ... -// } -// }) -// .catch((err) => { -// console.log('An error occurred while retrieving token. ', err); -// // ... -// }); messaging.onBackgroundMessage((payload) => { console.log('[firebase-messaging-sw.js] Received background message ', payload); // Customize notification here - const notificationTitle = 'Background Message Title'; - const notificationOptions = { - body: 'Background Message body.', - icon: '/firebase-logo.png', - }; + self.registration.showNotification(payload.notification.title, { + body: payload.notification.body, + data: payload.fcmOptions.link, + }); +}); - self.registration.showNotification(notificationTitle, notificationOptions); +self.addEventListener('notificationclick', (event) => { + const { data } = event.notification; + const [type, id] = data.split('='); + let clientFound = false; + console.log({ event }); + event.notification.close(); + event.waitUntil( + clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => { + console.log({ clients, clientList }); + for (let i = 0; i < clientList.length; i++) { + const client = clientList[i]; + if (client.visibilityState === 'visible') { + client.postMessage({ + action: 'notificationClick', + data + }); + clientFound = true; + break; + } + } + if (!clientFound) { + clients + .openWindow( + `https://localhost:8080/${type === 'events' ? 'main' : 'notification'}?${type}=${id}` + ); + } + }) + ); }); diff --git a/src/App.tsx b/src/App.tsx index ad9098d..64a314f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,11 @@ /* eslint-disable react/jsx-props-no-spreading */ +// eslint-disable-next-line import/no-extraneous-dependencies +import { initializeApp } from '@firebase/app'; import { Modal } from '@mui/material'; +import { Unsubscribe, getMessaging, onMessage } from 'firebase/messaging'; import { useContext, useEffect } from 'react'; -import { Navigate, Route, Routes, useSearchParams } from 'react-router-dom'; +import { Navigate, Route, Routes, useNavigate, useSearchParams } from 'react-router-dom'; import Wrapper from './AppStyles'; -import { useUserInfoQuery } from './api/query'; import BottomNavbar from './components/BottomNavbar'; import HeaderLogo from './components/HeaderLogo'; import { userContext } from './context'; @@ -15,37 +17,91 @@ import NoticePage from './pages/NoticePage'; import { SetMajor, Signup } from './pages/SignupPage'; import TerracePage from './pages/TerracePage'; +const firebaseConfig = { + apiKey: 'AIzaSyAEU-CHEmYMLlSLnRLpmoABONwqHZGVREw', + authDomain: 'test-csey.firebaseapp.com', + projectId: 'test-csey', + storageBucket: 'test-csey.appspot.com', + messagingSenderId: '491214968405', + appId: '1:491214968405:web:08e469439f00c3c07ba6dc', + measurementId: 'G-JGZP14R2TG', +}; +const app = initializeApp(firebaseConfig); + function App() { console.log('APP rerender'); - const { userData, dispatch } = useContext(userContext); + const { userData } = useContext(userContext); const { modal, modalDispatch } = useContext(ModalContext); const [, setSearchParams] = useSearchParams(); - useUserInfoQuery(userData, dispatch); + const navigate = useNavigate(); const { Component, props } = modal; const handleModalClose = () => { setSearchParams({}); modalDispatch({ payload: 'close' }); }; useEffect(() => { + let unsub: Unsubscribe; + let registration: ServiceWorkerRegistration; if ('serviceWorker' in navigator) { - navigator.serviceWorker - .register('/public/firebaseSW.js') - .then((registration) => { - console.log('Service Worker registered with scope:', registration.scope); - }) - .catch((error) => { - console.log('Service Worker registration failed:', error); - }) - .finally(() => { - console.log('finally'); + // TODO: onMessage에서 notification 나오게 만드는 코드 작성하기 + const regist = async () => { + console.log( + 'unreg:', + await navigator.serviceWorker.getRegistration('https://localhost:8080/public/') + ); + registration = await navigator.serviceWorker.register('/public/firebaseSW.js'); + if (registration) { + console.log('registration success', registration.scope, registration.active); + registration.showNotification('registration success', { + body: 'registration success body', + data: 'naver.com', + }); + } + navigator.serviceWorker.addEventListener('message', (event) => { + if (!event.data.action) return; + const { data, action } = event.data; + console.log('data:', event.data); + const [type, id] = (data as string).split('='); + switch (action) { + case 'notificationClick': + navigate(`/${type === 'events' ? 'main' : 'notification'}`); + modalDispatch({ + payload: 'mainModal', + props: { + postId: +id, + type: type as any + } + }); + break; + // skip default + } + }); + const messaging = getMessaging(app); + unsub = onMessage(messaging, (payload) => { + console.log({ payload }); + registration.showNotification(payload.notification.title, { + body: payload.notification.body, + data: payload.fcmOptions.link, + }); }); + }; + regist(); } + return () => { + if (unsub) { + console.log('clean'); + unsub(); + registration.unregister(); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( } /> + } /> } /> } /> } /> diff --git a/src/api/axios/alarm.ts b/src/api/axios/alarm.ts index 98c453d..0b02047 100644 --- a/src/api/axios/alarm.ts +++ b/src/api/axios/alarm.ts @@ -6,11 +6,17 @@ export type SetAlarmBody = { fcmToken: string; }; -function setAlarm({ alarmData, fcmToken }: SetAlarmBody) { +export function setAlarm({ alarmData, fcmToken }: SetAlarmBody) { console.log('set alarm called'); const loginInfo = JSON.parse(localStorage.getItem('info')); return axios.put(`/${loginInfo.userId}/alarms`, { alarmData, fcmToken }); } -export default setAlarm; + +export function retireToken(token: string) { + console.log('retireToken called'); + return axios.post('/fcm-token/retire', { + token + }); +} diff --git a/src/api/axios/index.ts b/src/api/axios/index.ts index 0174422..fad0310 100644 --- a/src/api/axios/index.ts +++ b/src/api/axios/index.ts @@ -3,4 +3,5 @@ import { getUserInfo, kakaoLogout, login } from './user'; axios.defaults.baseURL = '/api'; axios.defaults.withCredentials = true; + export { getUserInfo, kakaoLogout, login }; diff --git a/src/api/query/alarm.ts b/src/api/query/alarm.ts index c7b1bcd..31396a4 100644 --- a/src/api/query/alarm.ts +++ b/src/api/query/alarm.ts @@ -1,6 +1,6 @@ /* eslint-disable max-len */ import { useMutation } from '@tanstack/react-query'; -import setAlarm from '../axios/alarm'; +import { setAlarm } from '../axios/alarm'; const useAlarmMutation = () => { // const queryClient = useQueryClient(); diff --git a/src/api/query/index.ts b/src/api/query/index.ts index 3caa869..e0a6df7 100644 --- a/src/api/query/index.ts +++ b/src/api/query/index.ts @@ -7,7 +7,6 @@ import { useLoginQuery, useUserInfoQuery } from './user'; export { useAlarmMutation, - useNoticeByIdQuery as useAlertByIdQuery, useAlertsQuery, useEventBookmarkMutation, useEventByIdQuery, diff --git a/src/api/query/user.ts b/src/api/query/user.ts index 76956f9..bc4b746 100644 --- a/src/api/query/user.ts +++ b/src/api/query/user.ts @@ -1,10 +1,9 @@ -import { useQuery, useQueryClient } from '@tanstack/react-query'; -import { AxiosError } from 'axios'; -import { useEffect } from 'react'; -import { Action, UserData } from '../../context'; +import { useQuery } from '@tanstack/react-query'; +import axios, { AxiosError } from 'axios'; +import { UserData } from '../../context'; import { User } from '../../context/user.d'; import { login } from '../axios'; -import { getAccessToken, getUserInfo } from '../axios/user'; +import { getUserInfo } from '../axios/user'; export const useLoginQuery = (id: number, kakao_accessToken: string) => { const info = useQuery({ @@ -17,19 +16,17 @@ export const useLoginQuery = (id: number, kakao_accessToken: string) => { return info; }; -export const useUserInfoQuery = (userData: UserData, dispatch: React.Dispatch) => { +export const useUserInfoQuery = (userData: UserData) => { const { userId, isAuthenticated } = userData; - const queryClient = useQueryClient(); + // const queryClient = useQueryClient(); const userInfo = useQuery({ queryKey: ['userInfo'], queryFn: () => getUserInfo(userId), - enabled: isAuthenticated, + enabled: isAuthenticated && Boolean((axios.interceptors.response as any).handlers.length), staleTime: Infinity, select(data) { - const { - alarm, bookmark, eventLike, noticeLike, noticeRead, - } = data; - return new User(userData, alarm, bookmark, noticeRead, noticeLike, eventLike); + const { bookmark, eventLike, noticeLike, noticeRead } = data; + return new User(userData, bookmark, noticeRead, noticeLike, eventLike); }, retry(_failureCount, error: AxiosError) { // 401 == accessToken이 없음 @@ -40,28 +37,10 @@ export const useUserInfoQuery = (userData: UserData, dispatch: React.Dispatch 재시도 종료 후 error 플래그on const maxRetry = _failureCount === 3; const retryStatus = ![401, 419].some((errorCode) => errorCode === error?.response?.status); + console.count('retry'); + console.log({ retryStatus }); return maxRetry || retryStatus; }, }); - const { isError, error } = userInfo; - useEffect(() => { - const fetchNewAccessToken = async () => { - try { - const refreshToken = localStorage.getItem('refreshToken'); - await getAccessToken(refreshToken); - // 쿠키에 저장된 새로운 AccessToken으로 이전에 실패한 쿼리 다시 실행 - await queryClient.invalidateQueries({ queryKey: ['userInfo'] }); - } catch (_error) { - // RefreshToken이 만료된 경우 context정보 삭제 - dispatch({ payload: 'logout' }); - } - }; - - if (isError) { - fetchNewAccessToken(); - } - if (error?.status === 401) dispatch({ payload: 'logout' }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isError]); return userInfo; }; diff --git a/src/components/Login/index.tsx b/src/components/Login/index.tsx index 7a6c747..ae93221 100644 --- a/src/components/Login/index.tsx +++ b/src/components/Login/index.tsx @@ -2,8 +2,8 @@ import { memo, useContext, useEffect, useRef, useState } from 'react'; import KakaoLogin from 'react-kakao-login'; import { Props } from 'react-kakao-login/lib/types'; import { useLoginQuery } from '../../api/query'; -import { userContext } from '../../context'; import ChatYellow from '../../assets/Img/ChatYellow.png'; +import { userContext } from '../../context'; import * as s from '../../pages/SignupPage/styles'; type LoginButtonProps = { @@ -36,7 +36,7 @@ const LoginButton = memo(({ showLogin }: LoginButtonProps) => { dispatch({ payload: 'authenticate', data: { - info: { userId: id, name, major }, + info: { userId: id, name, major, fcmToken: localStorage.getItem('fcmToken') }, refreshToken: data.refreshToken, }, }); diff --git a/src/components/Modal/MainModal/index.tsx b/src/components/Modal/MainModal/index.tsx index a278725..2caf468 100644 --- a/src/components/Modal/MainModal/index.tsx +++ b/src/components/Modal/MainModal/index.tsx @@ -1,9 +1,7 @@ /* eslint-disable consistent-return */ import { forwardRef, useContext, useState } from 'react'; -import { Modal } from '@mui/material'; import { useSearchParams } from 'react-router-dom'; import { - useAlertByIdQuery, useEventBookmarkMutation, useEventByIdQuery, useEventLikeMutation, @@ -24,20 +22,19 @@ import ShareIcon from '../../../assets/Icons/ShareIcon.png'; import CloseBtnSrc from '../../../assets/Icons/modalCloseBtn.png'; import { userContext } from '../../../context'; import { formatDate } from '../../../util/dateUtil'; -import ModalImage from '../ModalImage'; import Toast from '../../Toast'; +import ModalImage from '../ModalImage'; import Stepper from '../stepper'; import * as s from './styles'; // eslint-disable-next-line import/no-cycle -import { ModalViewType } from '../../../pages/NoticePage'; import { MainModalProps, ModalContext } from '../../../context/modal'; +import { ModalViewType } from '../../../pages/NoticePage'; function useFetchDataByType(type: ModalViewType) { switch (type) { case 'events': return useEventByIdQuery; case 'alerts': - return useAlertByIdQuery; case 'notices': return useNoticeByIdQuery; // skip default @@ -59,10 +56,10 @@ const MainModal = forwardRef((_props, _ref) => { const { modal, modalDispatch } = useContext(ModalContext); const { type, postId: eventId } = (modal.props as MainModalProps); - const { userData, dispatch } = useContext(userContext); + const { userData } = useContext(userContext); const { userId } = userData; const { data, isPending } = useFetchDataByType(type)(eventId.toString()); - const { data: user } = useUserInfoQuery(userData, dispatch); + const { data: user } = useUserInfoQuery(userData); const isLike = user?.[type === 'events' ? 'eventsLike' : 'noticesLike']?.[eventId]; const [toast, setToast] = useState(false); const [curImgIdx, setCurImgIdx] = useState(0); @@ -196,6 +193,6 @@ const MainModal = forwardRef((_props, _ref) => { {toast && } ); -}) +}); export default MainModal; diff --git a/src/components/PaperList/AlarmSettingList/index.tsx b/src/components/PaperList/AlarmSettingList/index.tsx deleted file mode 100644 index 0c6cbbb..0000000 --- a/src/components/PaperList/AlarmSettingList/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { AlarmSettingCardContents } from '../../../context/user'; -import { - Content, - ContentGroup, ContentRow, - Description, - Meta, -} from '../styles'; - -function AlarmSettingList({ content }: { content: AlarmSettingCardContents[] }) { - if (!content) return <>loading; - return ( - - {content.map((v) => ( - - v.eventHandler(e)}> - {v.meta} - {v?.description} - - - ))} - - ); -} - -export default AlarmSettingList; diff --git a/src/context/index.tsx b/src/context/index.tsx index fb65b01..482ca19 100644 --- a/src/context/index.tsx +++ b/src/context/index.tsx @@ -1,5 +1,10 @@ +import axios, { AxiosError } from 'axios'; import { Dispatch, ReactNode, createContext, useEffect, useMemo, useReducer } from 'react'; import { useNavigate } from 'react-router-dom'; +import { retireToken } from '../api/axios/alarm'; +import { getAccessToken } from '../api/axios/user'; +import { useUserInfoQuery } from '../api/query'; +import requestFCMToken from '../util/firebaseCloudMessage'; interface SetLoginInfoAction { payload: 'authenticate'; @@ -16,17 +21,40 @@ interface LogoutAction { payload: 'logout'; data?: undefined; } +interface SetFcmToken { + payload: 'setFcmToken'; + data: string; +} export type UserData = { isAuthenticated: boolean; userId: number; name: string; major: SetMajorAction['data']; + fcmToken: string; }; -export type Action = SetLoginInfoAction | SetMajorAction | LogoutAction; +export type Action = SetLoginInfoAction | SetMajorAction | LogoutAction | SetFcmToken; export const userContext = createContext<{ userData: UserData; dispatch: Dispatch }>(null); const info = localStorage.getItem('info'); const parsedInfo = info && JSON.parse(info); +const fcmToken = localStorage.getItem('fcmToken'); +const alarm = localStorage.getItem('alarm'); +if (!alarm) { + localStorage.setItem( + 'alarm', + JSON.stringify({ + alarm_push: false, + event_push: false, + events_timer: 24, + events_form: '전체', + events_post: false, + major_schedule_push: false, + major_schedule_post: false, + notice_push: false, + alerts_push: false, + }) + ); +} export function UserProvider({ children }: { children: ReactNode }) { const [userData, dispatch] = useReducer(reducer, { @@ -34,10 +62,69 @@ export function UserProvider({ children }: { children: ReactNode }) { userId: parsedInfo?.userId, name: parsedInfo?.name, major: parsedInfo?.major, + fcmToken, }); + const userInfo = useUserInfoQuery(userData); + console.log({ userData }); const navigate = useNavigate(); const value = useMemo(() => ({ userData, dispatch }), [userData]); + + // token 갱신 + useEffect(() => { + axios.interceptors.response.use( + (response) => response, + async (error: AxiosError) => { + const originalRequest = error.config; + console.log({ originalRequest }); + switch (error.response.status) { + // access 없음 + // access 만료시 갱신 + case 401: + case 419: { + const refreshToken = localStorage.getItem('refreshToken'); + if (refreshToken) { + await getAccessToken(refreshToken); + return axios(originalRequest); + } + dispatch({ payload: 'logout' }); + break; + } + // refresh 없음 + case 403: { + dispatch({ payload: 'logout' }); + break; + } + // skip default + } + return Promise.reject(error); + } + ); + }, [dispatch]); + + // fcm토큰 갱신 + useEffect(() => { + const checkFcmToken = async () => { + const prevToken = fcmToken; + const currentToken = await requestFCMToken(); + if (!currentToken) { + console.warn('request FCM Token failed'); + return; + } + if (prevToken !== currentToken) { + if (prevToken) await retireToken(prevToken); + dispatch({ + payload: 'setFcmToken', + data: currentToken, + }); + } + }; + console.log({ userInfo }); + if (userInfo.isSuccess) checkFcmToken(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [userInfo?.isSuccess]); + + // 초기화면(회원가입) navigate useEffect(() => { if (!userData.isAuthenticated && !userData.major) { navigate('/signup'); @@ -65,7 +152,7 @@ function reducer(state: UserData, { payload, data }: Action): UserData { } case 'setMajor': { const { userId, name } = state; - localStorage.setItem('info', JSON.stringify({ userId, name, major: data })); + localStorage.setItem('info', JSON.stringify({ userId, name, fcmToken, major: data })); return { ...state, major: data }; } case 'logout': { @@ -74,6 +161,10 @@ function reducer(state: UserData, { payload, data }: Action): UserData { localStorage.removeItem('refreshToken'); return { ...state, userId: undefined, name: undefined, isAuthenticated: false }; } + case 'setFcmToken': { + localStorage.setItem('fcmToken', data); + return { ...state, fcmToken: data }; + } // skip default } } diff --git a/src/context/setting.ts b/src/context/setting.ts deleted file mode 100644 index 3d15670..0000000 --- a/src/context/setting.ts +++ /dev/null @@ -1,129 +0,0 @@ -/* eslint-disable max-classes-per-file */ -import { UseMutationResult } from '@tanstack/react-query'; -import { AxiosResponse } from 'axios'; -import { ChangeEvent } from 'react'; -import { SetAlarmBody } from '../api/axios/alarm'; -import { AlarmSettingCardContents, IAlarm, SettingCardData, SettingCardHeader } from './user'; - -type Mutation = UseMutationResult, Error, SetAlarmBody, unknown>; -const textMap = { - alarm_push: '전체 알림 켜기', - event_push: '행사 알림 켜기', - events_timer: '알림 시간', - events_form: '알림 형태', - events_post: '행사 등록 알림 켜기', - major_schedule_push: '일정 당일 알림 켜기', - major_schedule_post: '일정 등록 알림 켜기', - notice_push: '일반 공지 알림 켜기', - alerts_push: '긴급 공지 알림', -} as const; - -export class AlarmSettingContents { - private alarm_push: AlarmGroup; - - private event_push: AlarmGroup; - - private major_push: AlarmGroup; - - private notice_push: AlarmGroup; - - private entireAlarm: boolean; - - constructor(alarmData: IAlarm) { - // 개별 알람 - // 전체 - const alarmPush = new AlarmContent(undefined, 'alarm_push', alarmData); - this.entireAlarm = !alarmData.alarm_push; - // 행사 알림 - const eventsTimer = new AlarmContent(undefined, 'events_timer', alarmData, `시작 ${alarmData.events_timer}시간 전`); - const eventsForm = new AlarmContent(undefined, 'events_form', alarmData, `${alarmData.events_form}`); - const eventPush = new AlarmContent(alarmPush, 'event_push', alarmData, undefined, eventsTimer, eventsForm); - // 행사 등록 알림 - const eventsPost = new AlarmContent(alarmPush, 'events_post', alarmData); - // 학사일정 - const majorSchedulePost = new AlarmContent(alarmPush, 'major_schedule_post', alarmData); - const majorSchedulePush = new AlarmContent(alarmPush, 'major_schedule_push', alarmData); - // 공지 - const noticePush = new AlarmContent(alarmPush, 'notice_push', alarmData); - const alertsPush = new AlarmContent(alarmPush, 'alerts_push', alarmData); - - // 알람 그룹 - this.alarm_push = new AlarmGroup('전체 알림', alarmPush); - this.event_push = new AlarmGroup('행사 알림', eventPush, eventsPost); - this.major_push = new AlarmGroup('학사일정 알림', majorSchedulePost, majorSchedulePush); - this.notice_push = new AlarmGroup('공지 알림', noticePush, alertsPush); - - console.log('AlarmSettingContents: ', this); - this.alarm_push = new AlarmGroup('전체 알림', alarmPush); - this.event_push = new AlarmGroup('행사 알림', eventPush, eventsPost); - this.major_push = new AlarmGroup('학사일정 알림', majorSchedulePost, majorSchedulePush); - this.notice_push = new AlarmGroup('공지 알림', noticePush, alertsPush); - } - - getGroups() { - return [this.alarm_push, this.event_push, this.major_push, this.notice_push]; - } -} - -export class AlarmGroup implements SettingCardData { - header: SettingCardHeader; - - contents?: AlarmContent[]; - - constructor(header: string, ...contents: AlarmContent[]) { - this.header = { - title: header, - }; - this.contents = contents; - } - - setMutations(mutation: Mutation) { - this.contents.forEach((c) => { - c.setMutation(mutation); - }); - } - - findContent(key: string) { - return this.contents.find((c) => c.key === key); - } -} - -export class AlarmContent implements AlarmSettingCardContents { - meta: string; - - value?: any; - - hasCheck?: boolean; - - child?: AlarmContent[]; - - description?: string; - - disabled?: boolean; - - eventHandler?: (e?: ChangeEvent) => void; - - constructor( - public parent: AlarmContent, - public key: keyof typeof textMap, - alarmData: IAlarm, - description?: string, - ...child: AlarmContent[] - ) { - this.value = alarmData[this.key]; - this.meta = textMap[key]; - this.hasCheck = !description; - this.description = description; - this.child = child; - if (child) { - // eslint-disable-next-line no-param-reassign - child.forEach((c) => { c.parent = parent; }); - } - } - - setMutation(mutation: Mutation) { - this.eventHandler = (e: ChangeEvent) => { - // mutation.mutate({ [this.key]: this.hasCheck ? e.target.checked : e.target.value }); - }; - } -} diff --git a/src/context/user.d.ts b/src/context/user.d.ts index a103522..978f40d 100644 --- a/src/context/user.d.ts +++ b/src/context/user.d.ts @@ -16,7 +16,10 @@ export interface SettingCardContents { export interface AlarmSettingCardContents extends SettingCardContents { hasCheck?: boolean; // true: 체크박스 표시, false: └─> 표시 child?: SettingCardContents[]; - eventHandler?: (e: string) => (e: ChangeEvent | MouseEvent) => void; + eventHandler?: ( + e: ChangeEvent | MouseEvent, + name: string + ) => void; disabled?: boolean; } @@ -30,8 +33,6 @@ export type SettingCardData = { export class User { public user: IUser; - public alarm: IAlarm; - public bookmark: IBookmark; public noticeRead: IRead; @@ -42,7 +43,6 @@ export class User { constructor( user: UserData, - alarm?: IAlarm, bookmark?: IBookmark, reads?: IRead, noticesLike?: INoticeLike, diff --git a/src/pages/MainPage/EventCard/index.tsx b/src/pages/MainPage/EventCard/index.tsx index 2447e44..bf86172 100644 --- a/src/pages/MainPage/EventCard/index.tsx +++ b/src/pages/MainPage/EventCard/index.tsx @@ -3,10 +3,10 @@ import { useEventBookmarkMutation, useUserInfoQuery } from "../../../api/query"; import AlarmOffIcon from "../../../assets/Icons/AlarmOff.png"; import AlarmOnIcon from "../../../assets/Icons/AlarmOn.png"; import { userContext } from "../../../context"; +import { ModalContext } from "../../../context/modal"; import { EventType } from "../../../types"; import { leftDate } from "../../../util/dateUtil"; import * as s from "./styles"; -import { ModalContext } from "../../../context/modal"; // react 컴포넌트는 무조건 매개변수를 하나 갖고 있음(props) // props의 속성은 자유자재로 가능 @@ -16,10 +16,10 @@ function EventCard({ }: { event: EventType; }) { - const { userData, dispatch } = useContext(userContext); + const { userData } = useContext(userContext); const { modalDispatch } = useContext(ModalContext); - const { userId, isAuthenticated } = userData; - const { data } = useUserInfoQuery(userData, dispatch); + const { userId } = userData; + const { data } = useUserInfoQuery(userData); const alarmOn = data?.bookmark?.includes(id.toString()); const bookmarkMutation = useEventBookmarkMutation( userId?.toString(), diff --git a/src/pages/MyPage/index.tsx b/src/pages/MyPage/index.tsx index 390eba9..4dc964b 100644 --- a/src/pages/MyPage/index.tsx +++ b/src/pages/MyPage/index.tsx @@ -1,14 +1,13 @@ /* eslint-disable react/no-unused-prop-types */ /* eslint-disable react/jsx-props-no-spreading */ import { useQueryClient } from '@tanstack/react-query'; -import { memo, useContext, useEffect, useState } from 'react'; +import { memo, useContext, useState } from 'react'; import { ArrowLeft, Settings } from 'react-feather'; import { useNavigate } from 'react-router-dom'; import { useAlarmMutation } from '../../api/query'; import * as BottomNav from '../../components/BottomNavbar'; import PaperBox from '../../components/PaperBox'; import SettingList from '../../components/PaperList/SettingList'; -import { initializeApp } from '@firebase/app'; import { AccountSection, GuideSection, @@ -17,7 +16,6 @@ import { } from '../../components/SettingSection'; import { userContext } from '../../context'; import { ModalContext } from '../../context/modal'; -import { AlarmGroup } from '../../context/setting'; import { AlarmSettingCardContents, IAlarm } from '../../context/user'; import { AlarmWrapper, @@ -34,12 +32,10 @@ import { Title, Wrapper, } from './styles'; -import { requestFirebaseNotificationPermission } from '../../util/firebaseCloudMessage'; -import { getMessaging, onMessage } from 'firebase/messaging'; /** My - 설정페이지 */ export function Setting() { - const { modal, modalDispatch } = useContext(ModalContext); + const { modalDispatch } = useContext(ModalContext); const { userData, dispatch } = useContext(userContext); const navigate = useNavigate(); const client = useQueryClient(); @@ -65,16 +61,7 @@ export function Setting() { ); } -const firebaseConfig = { - apiKey: 'AIzaSyAEU-CHEmYMLlSLnRLpmoABONwqHZGVREw', - authDomain: 'test-csey.firebaseapp.com', - projectId: 'test-csey', - storageBucket: 'test-csey.appspot.com', - messagingSenderId: '491214968405', - appId: '1:491214968405:web:08e469439f00c3c07ba6dc', - measurementId: 'G-JGZP14R2TG', -}; -const app = initializeApp(firebaseConfig); + const textMap = { alarm_push: '전체 알림 켜기', event_push: '행사 알림 켜기', @@ -84,59 +71,32 @@ const textMap = { major_schedule_push: '일정 당일 알림 켜기', major_schedule_post: '일정 등록 알림 켜기', notice_push: '일반 공지 알림 켜기', - alerts_push: '긴급 공지 알림', + alerts_push: '긴급 공지 알림 켜기', } as const; /** My - 알림 설정 페이지 */ export function AlarmSetting() { - const { userData, dispatch } = useContext(userContext); + const { + userData: { fcmToken }, + } = useContext(userContext); const navigate = useNavigate(); const alarmMutation = useAlarmMutation(); - const [alarmData, setAlarmData] = useState({ - alarm_push: false, - event_push: false, - events_timer: 24, - events_form: '전체', - events_post: false, - major_schedule_push: false, - major_schedule_post: false, - notice_push: false, - alerts_push: false, - }); - const [fcmToken, setFcmToken] = useState(''); - useEffect(() => { - const subscribe = async () => { - const token = await requestFirebaseNotificationPermission(); - if (token) { - setFcmToken(token); - } - }; - - subscribe(); - - // const messaging = getMessaging(app); - // const unsub = onMessage(messaging, (payload) => { - // console.log('Message received. ', payload); - // }); - // return () => unsub(); - }, []); - const handleChange = (name) => (e) => { + const [alarmData, setAlarmData] = useState( + JSON.parse(localStorage.getItem('alarm')) + ); + const handleChange = (e, name: string) => { const { value, type, checked } = e.target; const next = { ...alarmData, [name]: type === 'checkbox' ? checked : value, }; - setAlarmData(next); + setAlarmData(() => { + localStorage.setItem('alarm', JSON.stringify(next)); + return next; + }); alarmMutation.mutate({ alarmData: next, fcmToken }); }; - const handleTokenChange = (e) => { - setFcmToken(e.target.value); - }; - - const handleSubmit = async (e) => { - e.preventDefault(); - }; return ( @@ -152,38 +112,38 @@ export function AlarmSetting() { - + - + - + - + {textMap[meta]} - + eventHandler(e, meta)} + /> ); } @@ -279,7 +245,7 @@ function ContentChild({ eventHandler, meta, description }: AlarmSettingCardConte return ( - + eventHandler(e, meta)}> {textMap[meta]} {description} diff --git a/src/pages/TerracePage/index.tsx b/src/pages/TerracePage/index.tsx index db1a0ce..1f6794a 100644 --- a/src/pages/TerracePage/index.tsx +++ b/src/pages/TerracePage/index.tsx @@ -1,66 +1,8 @@ -import { initializeApp } from '@firebase/app'; -import { getMessaging, onMessage } from 'firebase/messaging'; -import { useEffect, useState } from 'react'; -import { requestFirebaseNotificationPermission } from '../../util/firebaseCloudMessage'; import Wrapper from './styles'; -const firebaseConfig = { - apiKey: 'AIzaSyAEU-CHEmYMLlSLnRLpmoABONwqHZGVREw', - authDomain: 'test-csey.firebaseapp.com', - projectId: 'test-csey', - storageBucket: 'test-csey.appspot.com', - messagingSenderId: '491214968405', - appId: '1:491214968405:web:08e469439f00c3c07ba6dc', - measurementId: 'G-JGZP14R2TG', -}; -const app = initializeApp(firebaseConfig); - function TerracePage() { - const [notification, setNotification] = useState({ title: '', body: '' }); - const topic = 'test'; // 구독할 주제 - const topics = [ - 'test' - ]; - useEffect(() => { - const subscribe = async () => { - const token = await requestFirebaseNotificationPermission(); - // if (token) { - // await subscribeToTopic(token, topic); - // } - }; - - subscribe(); - - const messaging = getMessaging(app); - const unsub = onMessage(messaging, (payload) => { - console.log('Message received. ', payload); - setNotification({ - title: payload.notification.title, - body: payload.notification.body, - }); - }); - return () => unsub(); - }, []); - const onClickHandler = () => { - if (Notification.permission !== 'denied') { - Notification.requestPermission().then((permission) => { - if (permission === 'granted') { - console.log('granted', navigator.serviceWorker); - } else { - alert('Notification denied'); - } - }); - } - }; return ( - - -

Notification

-

{notification.title}

-

{notification.body}

-
+ ); } diff --git a/src/util/firebaseCloudMessage.ts b/src/util/firebaseCloudMessage.ts index 535dcc7..7de79f5 100644 --- a/src/util/firebaseCloudMessage.ts +++ b/src/util/firebaseCloudMessage.ts @@ -1,11 +1,11 @@ import { getMessaging, getToken } from 'firebase/messaging'; -export const requestFirebaseNotificationPermission = async () => { +const requestFCMToken = async () => { const messaging = getMessaging(); try { const token = await getToken(messaging, { serviceWorkerRegistration: await navigator.serviceWorker.getRegistration('https://localhost:8080/public/'), - vapidKey: 'BBHaXtLh_pZUn89NG0Jp1J_1N0wGS2R_9vllG20dfYsJ4dF5ZmTDofKD0HbBhoeZYhngL3YQ0GHUxRnnXpAkoko' + vapidKey: 'BBHaXtLh_pZUn89NG0Jp1J_1N0wGS2R_9vllG20dfYsJ4dF5ZmTDofKD0HbBhoeZYhngL3YQ0GHUxRnnXpAkoko', }); console.log('FCM Token:', token); return token; @@ -15,21 +15,4 @@ export const requestFirebaseNotificationPermission = async () => { } }; -export const subscribeToTopic = async (token, topic) => { - try { - const response = await fetch(`https://iid.googleapis.com/iid/v1/${token}/rel/topics/${topic}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (response.ok) { - console.log(`Subscribed to topic: ${topic}`); - } else { - console.error('Error subscribing to topic:', response.statusText); - } - } catch (error) { - console.error('Error subscribing to topic:', { error }); - } -}; +export default requestFCMToken;