Skip to content

Commit

Permalink
[Feat] QFEED-148 알림페이지 api 연동 (#81)
Browse files Browse the repository at this point in the history
* fix: qfeed-148 채팅 api 로그인 토큰 연결, 알림 api 세팅

* feat: qfeed-148 알림 api 연동

* fix: qfeed-148 알림리스트 타입 별 구분, 팔로우 버튼
  • Loading branch information
ahnjongin authored Dec 28, 2024
1 parent 29aee52 commit 3a8c9f7
Show file tree
Hide file tree
Showing 9 changed files with 296 additions and 123 deletions.
44 changes: 44 additions & 0 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { ThemeProvider } from '@emotion/react';
import { GlobalStyles } from '@/styles/GlobalStyles';
import theme from '@/styles/theme';
import { BottomNavigationStyleConfig as BottomNavigation } from 'chakra-ui-bottom-navigation';
import { initializeApp } from 'firebase/app';
import { getMessaging, getToken, onMessage } from 'firebase/messaging';

const queryClient = new QueryClient({
defaultOptions: {
Expand All @@ -24,11 +26,53 @@ const chakraTheme = extendTheme({
BottomNavigation,
},
});

// Firebase 초기화
const firebaseConfig = {
apiKey: 'AIzaSyCFC3QJKAZhz1R0k-h58wJA8_Rb_PbyiL4',
authDomain: 'q-feed.firebaseapp.com',
projectId: 'q-feed',
storageBucket: 'q-feed.firebasestorage.app',
messagingSenderId: '804246377517',
appId: '1:804246377517:web:71270af160949939da14a4',
};

const firebaseApp = initializeApp(firebaseConfig);
const messaging = getMessaging(firebaseApp);

// FCM 초기화 및 토큰 요청
const requestFCMToken = async () => {
try {
const token = await getToken(messaging, {
vapidKey:
'BKvBPha3ZSEI7Xb55-iWciONGqfKYtYgdj6kGWVe-mZDoeKYCCGwmAJaA12wl3zllzU5LCGX4Ar3_8Fix2QqEQ8', // Firebase 프로젝트 설정에서 VAPID 키 복사
});
if (token) {
console.log('FCM 토큰:', token);

// 백엔드에 FCM 토큰 저장 (필요 시)
// await axios.post('/api/save-token', { token });
} else {
console.warn('FCM 토큰을 가져올 수 없습니다. 알림 권한이 허용되지 않았을 수 있습니다.');
}
} catch (error) {
console.error('FCM 토큰 가져오기 실패:', error);
}
};

// FCM 포그라운드 메시지 처리
onMessage(messaging, (payload) => {
console.log('포그라운드 메시지 수신:', payload);
// TODO: 알림 UI 표시
});

// Service Worker 등록
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/firebase-messaging-sw.js')
.then((registration) => {
console.log('Service Worker 등록 성공:', registration);
requestFCMToken(); // Service Worker가 등록된 후 토큰 요청
})
.catch((error) => {
console.error('Service Worker 등록 실패:', error);
Expand Down
81 changes: 81 additions & 0 deletions src/pages/Alarm/AlarmItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/** @jsxImportSource @emotion/react */
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import ProfileImage from '@/components/ui/ProfileImageCon/ProfileImageCon';
import { NotificationItem } from '@/pages/Alarm/type/alarmType';
import {
listCon,
listConRead,
notificationContentStyle,
notificationMessageStyle,
notificationTypeStyle,
timeStyle,
followButtonStyle,
} from '@/pages/Alarm/styles';
import { markNotificationAsRead } from '@/pages/Alarm/api/fetchAlarm';

interface NotificationItemProps {
notification: NotificationItem; // 알림 데이터 타입
isRead: boolean; // 읽음 여부
onRead: (id: number) => void; // 읽음 처리 콜백 함수
}

const NotificationItemComponent: React.FC<NotificationItemProps> = ({
notification,
isRead,
onRead,
}) => {
const navigate = useNavigate();
const [isFollowing, setIsFollowing] = useState(false); // 팔로우 상태 관리

const handleFollow = () => {
if (isFollowing) {
console.log(`${notification.sender}를 팔로우 취소 요청`);
// TODO: 팔로우 취소 API 호출
setIsFollowing(false); // 상태 변경
} else {
console.log(`${notification.sender}를 맞팔로우 요청`);
// TODO: 맞팔로우 API 호출
setIsFollowing(true); // 상태 변경
}
};

const handleClick = async () => {
if (!isRead) {
await markNotificationAsRead(notification.notificationId); // 읽음 처리
onRead(notification.notificationId); // 부모 컴포넌트에 읽음 처리 콜백 전달
}

// URL이 있는 경우 해당 페이지로 이동
if (notification.url) {
navigate(notification.url);
}
};

return (
<div
css={[listCon, isRead && listConRead]} // 읽음 여부에 따라 스타일 변경
onClick={handleClick}
>
<ProfileImage src="" size={40} />
<div css={notificationContentStyle}>
<span css={notificationTypeStyle}>{notification.type}</span>
<p css={notificationMessageStyle}>{notification.content}</p>
{notification.type === 'FOLLOW' && (
<button
css={followButtonStyle}
onClick={(e) => {
e.stopPropagation(); // 부모 클릭 이벤트 차단
handleFollow(); // 팔로우/취소 처리
}}
>
{isFollowing ? '팔로잉' : '맞팔로우'}
</button>
)}
</div>
<span css={timeStyle}>{notification.time}</span>
</div>
);
};

export default NotificationItemComponent;
54 changes: 54 additions & 0 deletions src/pages/Alarm/api/fetchAlarm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { apiClient } from '@/api/fetch';
import { NotificationItem } from '@/pages/Alarm/type/alarmType';

export const notificationAPI = {
// 알림 목록 가져오기
getNotifications: () => apiClient.get<NotificationItem[]>('/notifications'),

// 알림 읽음 처리
markNotificationAsRead: (notificationId: number) =>
apiClient.put(`/notifications/${notificationId}/read`),

// 모든 알림 읽음 처리
markAllNotificationsAsRead: () => apiClient.put('/notifications/read-all'),
};

//알림 호출
export const fetchNotifications = async (): Promise<NotificationItem[]> => {
try {
const response = await notificationAPI.getNotifications();
console.log('알림 데이터: ', response.data); // 응답 데이터 확인
return response.data || [];
} catch (error) {
console.error('알림 조회 중 오류 발생: ', error); // 에러 로그
return [];
}
};

//알림 읽음 처리
export const markNotificationAsRead = async (notificationId: number): Promise<void> => {
try {
const response = await notificationAPI.markNotificationAsRead(notificationId);
if (response.status === 200) {
console.log(`알림 ${notificationId} 읽음 처리 완료`);
} else {
console.error(`알림 ${notificationId} 읽음 처리 실패`, response);
}
} catch (error) {
console.error('알림 읽음 처리 중 오류 발생:', error);
}
};

//알림 모두읽음 처리
export const markAllNotificationsAsRead = async (): Promise<void> => {
try {
const response = await notificationAPI.markAllNotificationsAsRead();
if (response.status === 200) {
console.log('모든 알림 읽음 처리 완료');
} else {
console.error('모든 알림 읽음 처리 실패', response);
}
} catch (error) {
console.error('모든 알림 읽음 처리 중 오류 발생:', error);
}
};
121 changes: 45 additions & 76 deletions src/pages/Alarm/index.tsx
Original file line number Diff line number Diff line change
@@ -1,116 +1,85 @@
/** @jsxImportSource @emotion/react */
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { IoChevronBack } from 'react-icons/io5';
import ProfileImage from '@/components/ui/ProfileImageCon/ProfileImageCon';
import NotificationItemComponent from '@/pages/Alarm/AlarmItem';
import {
backIconStyle,
containerStyle,
headerStyle,
headerTitleStyle,
listCon,
listConRead,
listStyle,
markAllAsReadStyle,
notificationContentStyle,
notificationMessageStyle,
notificationTypeStyle,
readWrap,
timeStyle,
unreadCountStyle,
markAllAsReadStyle,
listStyle,
} from '@/pages/Alarm/styles';
interface NotificationItem {
id: number;
type: string;
message: string;
time: string;
}
import { NotificationItem } from '@/pages/Alarm/type/alarmType';
import { fetchNotifications, markAllNotificationsAsRead } from '@/pages/Alarm/api/fetchAlarm';

const NotificationPage = () => {
const navigate = useNavigate();
const notifications: NotificationItem[] = [
{
id: 1,
type: '알림 유형(큐스페이스, 이벤트 등)',
message: "큐피드님이 나의 글에 '댓글'을 남겼습니다.",
time: '방금',
},
{
id: 2,
type: '알림 유형(큐스페이스, 이벤트 등)',
message: "큐피드님이 나의 글에 '댓글'을 남겼습니다.",
time: '1시간 전',
},
{
id: 3,
type: '알림 유형(큐스페이스, 이벤트 등)',
message: "큐피드님이 나의 글에 '댓글'을 남겼습니다.",
time: '1시간 전',
},
{
id: 4,
type: '알림 유형(큐스페이스, 이벤트 등)',
message: "큐피드님이 나의 글에 '댓글'을 남겼습니다.",
time: '방금',
},
{
id: 5,
type: '알림 유형(큐스페이스, 이벤트 등)',
message: "큐피드님이 나의 글에 '댓글'을 남겼습니다.",
time: '1시간 전',
},
{
id: 6,
type: '알림 유형(큐스페이스, 이벤트 등)',
message: "큐피드님이 나의 글에 '댓글'을 남겼습니다.",
time: '1시간 전',
},
];

// 알림 데이터 상태
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
const [readItems, setReadItems] = useState<number[]>([]); // 읽음 처리된 알림 ID 저장

const handleItemClick = (id: number) => {
setReadItems((prev) => (prev.includes(id) ? prev : [...prev, id]));
// 알림 데이터 불러오기
useEffect(() => {
const loadNotifications = async () => {
try {
const data = await fetchNotifications();
setNotifications(data);
const readIds = data
.filter((notification) => notification.isRead)
.map((n) => n.notificationId);
setReadItems(readIds); // 이미 읽음 처리된 알림 ID를 상태에 저장
} catch (error) {
console.error('알림 데이터를 불러오는 중 오류가 발생했습니다:', error);
}
};

loadNotifications();
}, []);

// 특정 알림 읽음 처리
const handleItemRead = (id: number) => {
setReadItems((prev) => [...prev, id]);
};

const markAllAsRead = () => {
setReadItems(notifications.map((notification) => notification.id));
// 모든 알림 읽음 처리
const handleMarkAllAsRead = async () => {
try {
await markAllNotificationsAsRead(); // API 호출
setReadItems(notifications.map((notification) => notification.notificationId)); // 모든 알림을 읽음 처리 상태로 변경
} catch (error) {
console.error('모든 알림 읽음 처리 중 오류가 발생했습니다:', error);
}
};

return (
<div css={containerStyle}>
{/* Header */}
<div css={headerStyle}>
<IoChevronBack css={backIconStyle} onClick={() => navigate(-1)} />
<IoChevronBack onClick={() => navigate(-1)} />
<span css={headerTitleStyle}>알림</span>
</div>

{/* Unread count */}
<div css={readWrap}>
<div css={unreadCountStyle}>안읽은 알림 {notifications.length - readItems.length}</div>
<span css={markAllAsReadStyle} onClick={markAllAsRead}>
<span css={markAllAsReadStyle} onClick={handleMarkAllAsRead}>
모두 읽음 표시
</span>
</div>

{/* Notification List */}
<div css={listStyle}>
{notifications.map((notification) => (
<div
key={notification.id}
css={[
listCon,
readItems.includes(notification.id) && listConRead, // 읽음 처리된 항목 스타일
]}
onClick={() => handleItemClick(notification.id)}
>
<ProfileImage src="" size={40} />
<div css={notificationContentStyle}>
<span css={notificationTypeStyle}>{notification.type}</span>
<p css={notificationMessageStyle}>{notification.message}</p>
</div>
<span css={timeStyle}>{notification.time}</span>
</div>
<NotificationItemComponent
key={notification.notificationId}
notification={notification}
isRead={readItems.includes(notification.notificationId)}
onRead={handleItemRead}
/>
))}
</div>
</div>
Expand Down
Loading

0 comments on commit 3a8c9f7

Please sign in to comment.