Merge branch 'fe/develop' of https://lab.ssafy.com/s11-s-project/S11P21S002 into fe/refactor/header
This commit is contained in:
commit
ec54881187
38
frontend/package-lock.json
generated
38
frontend/package-lock.json
generated
@ -12,6 +12,7 @@
|
|||||||
"@radix-ui/react-dialog": "^1.1.1",
|
"@radix-ui/react-dialog": "^1.1.1",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
|
"@radix-ui/react-popover": "^1.1.1",
|
||||||
"@radix-ui/react-radio-group": "^1.2.0",
|
"@radix-ui/react-radio-group": "^1.2.0",
|
||||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||||
"@radix-ui/react-select": "^2.1.1",
|
"@radix-ui/react-select": "^2.1.1",
|
||||||
@ -4296,6 +4297,43 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.0",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-context": "1.1.0",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.0",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.0",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.0",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-popper": "1.2.0",
|
||||||
|
"@radix-ui/react-portal": "1.1.1",
|
||||||
|
"@radix-ui/react-presence": "1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-slot": "1.1.0",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
|
"aria-hidden": "^1.1.1",
|
||||||
|
"react-remove-scroll": "2.5.7"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-popper": {
|
"node_modules/@radix-ui/react-popper": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
"@radix-ui/react-dialog": "^1.1.1",
|
"@radix-ui/react-dialog": "^1.1.1",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
|
"@radix-ui/react-popover": "^1.1.1",
|
||||||
"@radix-ui/react-radio-group": "^1.2.0",
|
"@radix-ui/react-radio-group": "^1.2.0",
|
||||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||||
"@radix-ui/react-select": "^2.1.1",
|
"@radix-ui/react-select": "^2.1.1",
|
||||||
|
22
frontend/src/api/alarmApi.ts
Normal file
22
frontend/src/api/alarmApi.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import api from '@/api/axiosConfig';
|
||||||
|
import { AlarmResponse } from '@/types';
|
||||||
|
|
||||||
|
export async function getAlarmList() {
|
||||||
|
return api.get<AlarmResponse[]>('/alarm').then(({ data }) => data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAlarmTest() {
|
||||||
|
return api.post('/alarm/test').then(({ data }) => data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readAlarm(alarmId: number) {
|
||||||
|
return api.put(`/alarm/${alarmId}`).then(({ data }) => data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAlarm(alarmId: number) {
|
||||||
|
return api.delete(`/alarm/${alarmId}`).then(({ data }) => data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAllAlarm() {
|
||||||
|
return api.delete('/alarm').then(({ data }) => data);
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import api from '@/api/axiosConfig';
|
import api from '@/api/axiosConfig';
|
||||||
import { MemberResponse, RefreshTokenResponse } from '@/types';
|
import { MemberResponse, RefreshTokenResponse } from '@/types';
|
||||||
|
import { getFcmToken } from './firebaseConfig';
|
||||||
|
|
||||||
export async function reissueToken() {
|
export async function reissueToken() {
|
||||||
return api.post<RefreshTokenResponse>('/auth/reissue', null, { withCredentials: true }).then(({ data }) => data);
|
return api.post<RefreshTokenResponse>('/auth/reissue', null, { withCredentials: true }).then(({ data }) => data);
|
||||||
@ -13,8 +14,9 @@ export async function logout() {
|
|||||||
return api.post('/auth/logout').then(({ data }) => data);
|
return api.post('/auth/logout').then(({ data }) => data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveFcmToken(token: string) {
|
export async function getAndSaveFcmToken() {
|
||||||
return api.post('/auth/fcm', { token }).then(({ data }) => data);
|
const fcmToken = await getFcmToken();
|
||||||
|
return api.post('/auth/fcm', { token: fcmToken }).then(({ data }) => ({ data, fcmToken }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createFcmNotification() {
|
export async function createFcmNotification() {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { initializeApp } from 'firebase/app';
|
import { initializeApp } from 'firebase/app';
|
||||||
import { getMessaging, getToken, onMessage } from 'firebase/messaging';
|
import { getMessaging, onMessage } from 'firebase/messaging';
|
||||||
|
|
||||||
const firebaseConfig = {
|
const firebaseConfig = {
|
||||||
apiKey: String(import.meta.env.VITE_FIREBASE_API_KEY),
|
apiKey: String(import.meta.env.VITE_FIREBASE_API_KEY),
|
||||||
@ -22,30 +22,31 @@ const getFcmToken = async () => {
|
|||||||
return existingToken;
|
return existingToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
const permission = await Notification.requestPermission();
|
// const permission = await Notification.requestPermission();
|
||||||
|
|
||||||
if (permission === 'granted') {
|
// if (permission === 'granted') {
|
||||||
console.log('알림 권한이 허용되었습니다.');
|
// console.log('알림 권한이 허용되었습니다.');
|
||||||
|
|
||||||
console.log('FCM 토큰 발급 중...');
|
// console.log('FCM 토큰 발급 중...');
|
||||||
const currentToken = await getToken(messaging, {
|
// const currentToken = await getToken(messaging, {
|
||||||
vapidKey: 'BApIruZrx83suCd09dnDCkFSP_Ts08q38trrIL6GHpChtbjQHTHk_38_JRyTiKLqciHxLQ8iXtie3lvgyb4Iphg',
|
// vapidKey: 'BApIruZrx83suCd09dnDCkFSP_Ts08q38trrIL6GHpChtbjQHTHk_x38_JRyTiKLqciHxLQ8iXtie3lvgyb4Iphg',
|
||||||
});
|
// });
|
||||||
console.log('FCM 토큰 발급 성공');
|
// console.log('FCM 토큰 발급 성공');
|
||||||
|
|
||||||
if (currentToken) {
|
// if (currentToken) {
|
||||||
sessionStorage.setItem('fcmToken', currentToken);
|
// sessionStorage.setItem('fcmToken', currentToken);
|
||||||
return currentToken;
|
// return currentToken;
|
||||||
}
|
// }
|
||||||
|
|
||||||
console.warn('FCM 토큰을 가져올 수 없습니다.');
|
// console.warn('FCM 토큰을 가져올 수 없습니다.');
|
||||||
} else {
|
// } else {
|
||||||
console.log('알림 권한이 거부되었습니다.');
|
// console.log('알림 권한이 거부되었습니다.');
|
||||||
}
|
// }
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.error('FCM 토큰을 가져오는 중 오류가 발생했습니다. : ', error);
|
// console.error('FCM 토큰을 가져오는 중 오류가 발생했습니다. : ', error);
|
||||||
}
|
// }
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleForegroundMessages = () => {
|
const handleForegroundMessages = () => {
|
||||||
|
74
frontend/src/components/Header/AlarmItem.tsx
Normal file
74
frontend/src/components/Header/AlarmItem.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { AlarmResponse } from '@/types';
|
||||||
|
import { Mail, MailOpen, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function AlarmItem({
|
||||||
|
alarm,
|
||||||
|
onRead,
|
||||||
|
onDelete,
|
||||||
|
}: {
|
||||||
|
alarm: AlarmResponse;
|
||||||
|
onRead: (alarmId: number) => void;
|
||||||
|
onDelete: (alarmId: number) => void;
|
||||||
|
}) {
|
||||||
|
const timeAgo = (date: string | Date) => {
|
||||||
|
const now = new Date();
|
||||||
|
const past = new Date(date);
|
||||||
|
const diffInSeconds = Math.floor((now.getTime() - past.getTime()) / 1000);
|
||||||
|
|
||||||
|
if (diffInSeconds < 60) return `${Math.max(diffInSeconds, 0)}초 전`;
|
||||||
|
const diffInMinutes = Math.floor(diffInSeconds / 60);
|
||||||
|
if (diffInMinutes < 60) return `${diffInMinutes}분 전`;
|
||||||
|
const diffInHours = Math.floor(diffInMinutes / 60);
|
||||||
|
if (diffInHours < 24) return `${diffInHours}시간 전`;
|
||||||
|
const diffInDays = Math.floor(diffInHours / 24);
|
||||||
|
return `${diffInDays}일 전`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRead = () => {
|
||||||
|
onRead(alarm.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
onDelete(alarm.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full items-center bg-white py-2 pr-[18px] duration-150 hover:bg-gray-200">
|
||||||
|
<div className={cn('mx-1.5 h-1.5 w-1.5 rounded-full', alarm.isRead ? 'bg-transparent' : 'bg-blue-500')}></div>
|
||||||
|
<div className="flex flex-1 flex-col">
|
||||||
|
<p className={cn('body-small', alarm.isRead ? 'text-gray-400' : 'text-black')}>
|
||||||
|
[{alarm.id}] {alarm.type} 알림입니다.
|
||||||
|
</p>
|
||||||
|
<p className="caption text-gray-500">{timeAgo(alarm.createdAt)}</p>
|
||||||
|
</div>
|
||||||
|
{alarm.isRead ? (
|
||||||
|
<button
|
||||||
|
className="p-1"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<MailOpen
|
||||||
|
size={16}
|
||||||
|
className="stroke-gray-400"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="p-1"
|
||||||
|
onClick={handleRead}
|
||||||
|
>
|
||||||
|
<Mail size={16} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="p-1"
|
||||||
|
onClick={handleDelete}
|
||||||
|
>
|
||||||
|
<Trash2
|
||||||
|
size={16}
|
||||||
|
className="stroke-red-500"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
137
frontend/src/components/Header/AlarmPopover.tsx
Normal file
137
frontend/src/components/Header/AlarmPopover.tsx
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Bell } from 'lucide-react';
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
||||||
|
import { onMessage } from 'firebase/messaging';
|
||||||
|
import { messaging } from '@/api/firebaseConfig';
|
||||||
|
import AlarmItem from './AlarmItem';
|
||||||
|
import useFcmTokenQuery from '@/queries/auth/useFcmTokenQuery';
|
||||||
|
import useGetAlarmListQuery from '@/queries/alarms/useGetAlarmListQuery';
|
||||||
|
import useResetAlarmListQuery from '@/queries/alarms/useResetAlarmListQuery';
|
||||||
|
import useCreateAlarmTestQuery from '@/queries/alarms/useCreateAlarmTestQuery';
|
||||||
|
import useReadAlarmQuery from '@/queries/alarms/useReadAlarmQuery';
|
||||||
|
import useDeleteAlarmQuery from '@/queries/alarms/useDeleteAlarmQuery';
|
||||||
|
import useDeleteAllAlarmQuery from '@/queries/alarms/useDeleteAllAlarmQuery';
|
||||||
|
|
||||||
|
export default function AlarmPopover() {
|
||||||
|
const [unread, setUnread] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const resetAlarmList = useResetAlarmListQuery();
|
||||||
|
const createAlarmTest = useCreateAlarmTestQuery();
|
||||||
|
const readAlarm = useReadAlarmQuery();
|
||||||
|
const deleteAlarm = useDeleteAlarmQuery();
|
||||||
|
const deleteAllAlarm = useDeleteAllAlarmQuery();
|
||||||
|
|
||||||
|
const handleResetAlarmList = () => {
|
||||||
|
resetAlarmList.mutate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateAlarmTest = () => {
|
||||||
|
createAlarmTest.mutate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReadAlarm = (alarmId: number) => {
|
||||||
|
readAlarm.mutate(alarmId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteAlarm = (alarmId: number) => {
|
||||||
|
deleteAlarm.mutate(alarmId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteAllAlarm = () => {
|
||||||
|
deleteAllAlarm.mutate();
|
||||||
|
};
|
||||||
|
|
||||||
|
useFcmTokenQuery();
|
||||||
|
const { data: alarms } = useGetAlarmListQuery();
|
||||||
|
|
||||||
|
onMessage(messaging, (payload) => {
|
||||||
|
if (!payload.data) return;
|
||||||
|
|
||||||
|
console.log('new message arrived');
|
||||||
|
handleResetAlarmList();
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unreadCnt = alarms.filter((alarm) => !alarm.isRead).length;
|
||||||
|
|
||||||
|
if (unreadCnt > 0) {
|
||||||
|
setUnread(true);
|
||||||
|
} else {
|
||||||
|
setUnread(false);
|
||||||
|
}
|
||||||
|
}, [alarms]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<button
|
||||||
|
className="flex items-center justify-center p-2"
|
||||||
|
onClick={() => {}}
|
||||||
|
>
|
||||||
|
<Bell className="h-4 w-4 cursor-pointer text-black sm:h-5 sm:w-5" />
|
||||||
|
<div className={cn('mt-[14px] h-1.5 w-1.5 rounded-full', unread ? 'bg-blue-500' : 'bg-transparent')}></div>
|
||||||
|
</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
|
||||||
|
<PopoverContent
|
||||||
|
className="w-80 overflow-hidden rounded-lg p-0"
|
||||||
|
align="end"
|
||||||
|
sideOffset={14}
|
||||||
|
alignOffset={0}
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center px-[18px] py-3">
|
||||||
|
<h2 className="body-strong flex-1">알림</h2>
|
||||||
|
<button
|
||||||
|
className="body-small p-1 text-blue-500"
|
||||||
|
onClick={handleCreateAlarmTest}
|
||||||
|
>
|
||||||
|
테스트
|
||||||
|
</button>
|
||||||
|
{/* {unread ? (
|
||||||
|
<button
|
||||||
|
className="body-small p-1"
|
||||||
|
onClick={() => {}}
|
||||||
|
>
|
||||||
|
모두 읽음
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="body-small p-1"
|
||||||
|
onClick={() => {}}
|
||||||
|
>
|
||||||
|
모두 읽지 않음
|
||||||
|
</button>
|
||||||
|
)} */}
|
||||||
|
<button
|
||||||
|
className="body-small p-1 text-red-500"
|
||||||
|
onClick={handleDeleteAllAlarm}
|
||||||
|
>
|
||||||
|
모두 삭제
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
{alarms.length === 0 ? (
|
||||||
|
<div className="flex w-full items-center px-[18px] py-3 duration-150">
|
||||||
|
<p className="body-small text-gray-500">알림이 없습니다.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex max-h-[500px] w-full flex-col items-center overflow-y-auto">
|
||||||
|
{alarms
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.map((alarm) => (
|
||||||
|
<AlarmItem
|
||||||
|
key={alarm.id}
|
||||||
|
alarm={alarm}
|
||||||
|
onRead={handleReadAlarm}
|
||||||
|
onDelete={handleDeleteAlarm}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
@ -1,10 +1,10 @@
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Bell } from 'lucide-react';
|
|
||||||
import { useLocation, Link } from 'react-router-dom';
|
import { useLocation, Link } from 'react-router-dom';
|
||||||
import UserProfileModal from './UserProfileModal';
|
import UserProfileModal from './UserProfileModal';
|
||||||
import WorkspaceNavigation from './WorkspaceNavigation';
|
import WorkspaceNavigation from './WorkspaceNavigation';
|
||||||
import useAuthStore from '@/stores/useAuthStore';
|
import useAuthStore from '@/stores/useAuthStore';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
import AlarmPopover from './AlarmPopover';
|
||||||
|
|
||||||
export interface HeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
|
export interface HeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
|
||||||
|
|
||||||
@ -39,8 +39,8 @@ export default function Header({ className, ...props }: HeaderProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isHomePage && profile && (
|
{!isHomePage && profile && (
|
||||||
<div className="flex items-center gap-4 md:gap-5">
|
<div className="flex items-center gap-2">
|
||||||
<Bell className="h-4 w-4 text-black sm:h-5 sm:w-5" />
|
<AlarmPopover />
|
||||||
<UserProfileModal />
|
<UserProfileModal />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -170,7 +170,7 @@ export default function ImageCanvas() {
|
|||||||
const id = labels.length;
|
const id = labels.length;
|
||||||
addLabel({
|
addLabel({
|
||||||
id: id,
|
id: id,
|
||||||
categoryId: 0,
|
categoryId: categories[0]!.id,
|
||||||
type: 'polygon',
|
type: 'polygon',
|
||||||
color: `#${color}`,
|
color: `#${color}`,
|
||||||
coordinates: polygonPoints.slice(0, -1),
|
coordinates: polygonPoints.slice(0, -1),
|
||||||
@ -200,7 +200,7 @@ export default function ImageCanvas() {
|
|||||||
const id = labels.length;
|
const id = labels.length;
|
||||||
addLabel({
|
addLabel({
|
||||||
id: id,
|
id: id,
|
||||||
categoryId: 0,
|
categoryId: categories[0]!.id,
|
||||||
type: 'rectangle',
|
type: 'rectangle',
|
||||||
color: `#${color}`,
|
color: `#${color}`,
|
||||||
coordinates: rectPoints,
|
coordinates: rectPoints,
|
||||||
|
29
frontend/src/components/ui/popover.tsx
Normal file
29
frontend/src/components/ui/popover.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Popover = PopoverPrimitive.Root
|
||||||
|
|
||||||
|
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||||
|
|
||||||
|
const PopoverContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||||
|
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 w-72 rounded-md border border-gray-200 bg-white p-4 text-gray-950 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-800 dark:bg-gray-950 dark:text-gray-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
))
|
||||||
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent }
|
@ -1,44 +0,0 @@
|
|||||||
import { createFcmNotification, saveFcmToken } from '@/api/authApi';
|
|
||||||
import { getFcmToken, handleForegroundMessages } from '@/api/firebaseConfig';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
|
|
||||||
export default function FirebaseTest() {
|
|
||||||
const handleSaveFcmToken = async () => {
|
|
||||||
const fcmToken = await getFcmToken();
|
|
||||||
|
|
||||||
if (fcmToken) {
|
|
||||||
await saveFcmToken(fcmToken);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('FCM 토큰이 없습니다.');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateNotification = async () => {
|
|
||||||
await createFcmNotification();
|
|
||||||
};
|
|
||||||
|
|
||||||
handleForegroundMessages();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 className="heading p-2">hello, firebase!</h1>
|
|
||||||
<div className="p-2">
|
|
||||||
<Button
|
|
||||||
onClick={handleSaveFcmToken}
|
|
||||||
variant="outlinePrimary"
|
|
||||||
className="mr-2"
|
|
||||||
>
|
|
||||||
FCM 토큰 서버에 저장
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleCreateNotification}
|
|
||||||
variant="outlinePrimary"
|
|
||||||
className="mr-2"
|
|
||||||
>
|
|
||||||
FCM 알림 테스트
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
13
frontend/src/queries/alarms/useCreateAlarmTestQuery.ts
Normal file
13
frontend/src/queries/alarms/useCreateAlarmTestQuery.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { createAlarmTest } from '@/api/alarmApi';
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export default function useCreateAlarmTestQuery() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: createAlarmTest,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['alarmList'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
13
frontend/src/queries/alarms/useDeleteAlarmQuery.ts
Normal file
13
frontend/src/queries/alarms/useDeleteAlarmQuery.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { deleteAlarm } from '@/api/alarmApi';
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export default function useDeleteAlarmQuery() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (alarmId: number) => deleteAlarm(alarmId),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['alarmList'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
13
frontend/src/queries/alarms/useDeleteAllAlarmQuery.ts
Normal file
13
frontend/src/queries/alarms/useDeleteAllAlarmQuery.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { deleteAllAlarm } from '@/api/alarmApi';
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export default function useDeleteAllAlarmQuery() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: deleteAllAlarm,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['alarmList'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
9
frontend/src/queries/alarms/useGetAlarmListQuery.ts
Normal file
9
frontend/src/queries/alarms/useGetAlarmListQuery.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { getAlarmList } from '@/api/alarmApi';
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export default function useGetAlarmListQuery() {
|
||||||
|
return useSuspenseQuery({
|
||||||
|
queryKey: ['alarmList'],
|
||||||
|
queryFn: getAlarmList,
|
||||||
|
});
|
||||||
|
}
|
13
frontend/src/queries/alarms/useReadAlarmQuery.ts
Normal file
13
frontend/src/queries/alarms/useReadAlarmQuery.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { readAlarm } from '@/api/alarmApi';
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export default function useReadAlarmQuery() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (alarmId: number) => readAlarm(alarmId),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['alarmList'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
12
frontend/src/queries/alarms/useResetAlarmListQuery.ts
Normal file
12
frontend/src/queries/alarms/useResetAlarmListQuery.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export default function useUpdateAlarmQuery() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async () => {},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['alarmList'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
9
frontend/src/queries/auth/useFcmTokenQuery.ts
Normal file
9
frontend/src/queries/auth/useFcmTokenQuery.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { getAndSaveFcmToken } from '@/api/authApi';
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export default function useFcmTokenQuery() {
|
||||||
|
return useSuspenseQuery({
|
||||||
|
queryKey: ['fcmToken'],
|
||||||
|
queryFn: getAndSaveFcmToken,
|
||||||
|
});
|
||||||
|
}
|
@ -20,7 +20,6 @@ import NotFound from '@/pages/NotFound';
|
|||||||
import ReviewRequest from '@/pages/ReviewRequest';
|
import ReviewRequest from '@/pages/ReviewRequest';
|
||||||
import ModelIndex from '@/pages/ModelIndex';
|
import ModelIndex from '@/pages/ModelIndex';
|
||||||
import ModelDetail from '@/pages/ModelDetail';
|
import ModelDetail from '@/pages/ModelDetail';
|
||||||
import FirebaseTest from '@/pages/FirebaseTest';
|
|
||||||
|
|
||||||
export const webPath = {
|
export const webPath = {
|
||||||
home: () => '/',
|
home: () => '/',
|
||||||
@ -28,7 +27,6 @@ export const webPath = {
|
|||||||
workspace: () => '/workspace',
|
workspace: () => '/workspace',
|
||||||
admin: () => `/admin`,
|
admin: () => `/admin`,
|
||||||
oauthCallback: () => '/redirect/oauth2',
|
oauthCallback: () => '/redirect/oauth2',
|
||||||
firebaseTest: () => '/firebaseTest',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
@ -151,14 +149,6 @@ const router = createBrowserRouter([
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: webPath.firebaseTest(),
|
|
||||||
element: (
|
|
||||||
<Suspense fallback={<div></div>}>
|
|
||||||
<FirebaseTest />
|
|
||||||
</Suspense>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
@ -348,3 +348,10 @@ export interface ReportResponse {
|
|||||||
leftSecond: number;
|
leftSecond: number;
|
||||||
segLoss: number;
|
segLoss: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AlarmResponse {
|
||||||
|
id: number;
|
||||||
|
isRead: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user