diff --git a/backend/build.gradle b/backend/build.gradle index 175194ec..f7552895 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -47,6 +47,7 @@ dependencies { implementation 'com.amazonaws:aws-java-sdk-core:1.12.681' implementation 'org.apache.commons:commons-lang3:3.12.0' implementation 'org.springframework.boot:spring-boot-starter-websocket' + implementation 'com.google.firebase:firebase-admin:9.2.0' } tasks.named('test') { diff --git a/backend/src/main/java/org/example/backend/BackendApplication.java b/backend/src/main/java/org/example/backend/BackendApplication.java index 20b95bc8..8352b654 100644 --- a/backend/src/main/java/org/example/backend/BackendApplication.java +++ b/backend/src/main/java/org/example/backend/BackendApplication.java @@ -4,9 +4,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableJpaAuditing +@EnableScheduling public class BackendApplication { public static void main(String[] args) { // .env 파일 로딩 (없어도 실행되게 ignoreIfMissing 추가) diff --git a/backend/src/main/java/org/example/backend/domain/lecture/repository/LectureRepository.java b/backend/src/main/java/org/example/backend/domain/lecture/repository/LectureRepository.java index 122852c8..979030d1 100644 --- a/backend/src/main/java/org/example/backend/domain/lecture/repository/LectureRepository.java +++ b/backend/src/main/java/org/example/backend/domain/lecture/repository/LectureRepository.java @@ -1,10 +1,13 @@ package org.example.backend.domain.lecture.repository; +import io.lettuce.core.dynamic.annotation.Param; import org.example.backend.domain.classroom.entity.Classroom; import org.example.backend.domain.lecture.entity.Lecture; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import java.time.LocalDate; +import java.time.LocalTime; import java.util.List; import java.util.UUID; @@ -15,4 +18,12 @@ public interface LectureRepository extends JpaRepository { List findByClassroomInAndLectureDate(List classrooms, LocalDate lectureDate); List findByClassroom_IdOrderByLectureDateAscStartTimeAsc(UUID classId); + @Query("SELECT l FROM Lecture l " + + "WHERE l.lectureDate = :today " + + "AND l.startTime = :targetTime " + + "AND l.isLectureStart = false") + List findLecturesStartingAt( + @Param("today") LocalDate today, + @Param("targetTime") LocalTime targetTime + ); } diff --git a/backend/src/main/java/org/example/backend/domain/lecture/service/LectureScheduler.java b/backend/src/main/java/org/example/backend/domain/lecture/service/LectureScheduler.java new file mode 100644 index 00000000..39048ec4 --- /dev/null +++ b/backend/src/main/java/org/example/backend/domain/lecture/service/LectureScheduler.java @@ -0,0 +1,42 @@ +package org.example.backend.domain.lecture.service; + +import lombok.RequiredArgsConstructor; +import org.example.backend.domain.lecture.entity.Lecture; +import org.example.backend.domain.lecture.repository.LectureRepository; +import org.example.backend.domain.notification.entity.AlarmType; +import org.example.backend.domain.notification.service.NotificationService; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class LectureScheduler { + + private final LectureRepository lectureRepository; + private final NotificationService notificationService; + + // 매 분마다 실행 + @Scheduled(cron = "0 * * * * *") + public void notifyProfessorBeforeLecture() { + LocalDate today = LocalDate.now(); + LocalTime now = LocalTime.now().withSecond(0).withNano(0); + + // 🔥 "현재 시각 + 10분"이 lecture start_time 인 강의 찾기 + LocalTime targetStartTime = now.plusMinutes(10); + + List lectures = lectureRepository.findLecturesStartingAt(today, targetStartTime); + + for (Lecture lecture : lectures) { + notificationService.sendAlarmToProfessor( + lecture.getId(), + AlarmType.startLecture, + "시스템", + lecture.getLectureName() + " 강의가 10분 후 시작됩니다." + ); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/org/example/backend/domain/notification/controller/NotificationController.java b/backend/src/main/java/org/example/backend/domain/notification/controller/NotificationController.java index 2ea10cea..ca8b83f3 100644 --- a/backend/src/main/java/org/example/backend/domain/notification/controller/NotificationController.java +++ b/backend/src/main/java/org/example/backend/domain/notification/controller/NotificationController.java @@ -6,6 +6,7 @@ import org.example.backend.global.ApiResponse; import org.example.backend.global.security.auth.CustomSecurityUtil; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -28,4 +29,10 @@ public ApiResponse> getNotifications() { List notifications = notificationService.getNotificationsByUserId(userId); return ApiResponse.onSuccess(notifications); } + + @PatchMapping("/read-all") + public void markAllAsRead() { + UUID userId = customSecurityUtil.getUserId(); + notificationService.markAllAsRead(userId); + } } diff --git a/backend/src/main/java/org/example/backend/domain/notification/repository/NotificationRepository.java b/backend/src/main/java/org/example/backend/domain/notification/repository/NotificationRepository.java index 3b324036..7979dd61 100644 --- a/backend/src/main/java/org/example/backend/domain/notification/repository/NotificationRepository.java +++ b/backend/src/main/java/org/example/backend/domain/notification/repository/NotificationRepository.java @@ -1,8 +1,11 @@ package org.example.backend.domain.notification.repository; +import io.lettuce.core.dynamic.annotation.Param; import org.example.backend.domain.classroom.entity.Classroom; import org.example.backend.domain.notification.entity.Notification; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.util.List; @@ -11,4 +14,8 @@ @Repository public interface NotificationRepository extends JpaRepository { List findByUserIdOrderByCreatedAtDesc(UUID userId); + + @Modifying + @Query("UPDATE Notification n SET n.isRead = true WHERE n.user.id = :userId") + void markAllAsReadByUserId(@Param("userId") UUID userId); } diff --git a/backend/src/main/java/org/example/backend/domain/notification/service/NotificationService.java b/backend/src/main/java/org/example/backend/domain/notification/service/NotificationService.java index 8fd9e294..2c8bf0a4 100644 --- a/backend/src/main/java/org/example/backend/domain/notification/service/NotificationService.java +++ b/backend/src/main/java/org/example/backend/domain/notification/service/NotificationService.java @@ -7,9 +7,14 @@ import org.example.backend.domain.lecture.repository.LectureRepository; import org.example.backend.domain.notification.converter.NotificationConverter; import org.example.backend.domain.notification.dto.response.NotificationResponseDTO; +import org.example.backend.domain.notification.entity.AlarmType; import org.example.backend.domain.notification.entity.Notification; import org.example.backend.domain.notification.repository.NotificationRepository; +import org.example.backend.domain.notificationSetting.service.FcmService; +import org.example.backend.domain.notificationSetting.service.NotificationTemplateService; +import org.example.backend.global.userdeviceToken.repository.UserDeviceTokenRepository; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.UUID; @@ -20,7 +25,10 @@ public class NotificationService implements NotificationServiceImpl{ private final NotificationRepository notificationRepository; private final LectureRepository lectureRepository; private final ClassroomRepository classroomRepository; - private final NotificationConverter notificationConverter;; + private final NotificationConverter notificationConverter; + private final NotificationTemplateService templateService; + private final UserDeviceTokenRepository tokenRepository; + private final FcmService fcmService; public List getNotificationsByUserId(UUID userId) { List notificationList = @@ -39,4 +47,32 @@ public List getNotificationsByUserId(UUID userId) { }) .toList(); } + + public void sendAlarmToProfessor(UUID lectureId, AlarmType type, String senderName, String extra) { + Lecture lecture = lectureRepository.findById(lectureId) + .orElseThrow(() -> new RuntimeException("Lecture not found")); + + UUID professorId = lecture.getClassroom().getProfessor().getId(); + + String title = templateService.getTitle(type); + String body = templateService.getBody(type, senderName, extra); + + var tokens = tokenRepository.findAllByUserIdAndIsActiveTrue(professorId); + tokens.forEach(token -> + fcmService.sendNotification(token.getFcmToken(), title, body) + ); + + Notification notification = Notification.builder() + .user(lecture.getClassroom().getProfessor()) + .lecture(lecture) + .alarmType(type) + .isRead(false) + .build(); + notificationRepository.save(notification); + } + + @Transactional + public void markAllAsRead(UUID userId) { + notificationRepository.markAllAsReadByUserId(userId); + } } diff --git a/backend/src/main/java/org/example/backend/domain/notificationSetting/entity/NotificationSetting.java b/backend/src/main/java/org/example/backend/domain/notificationSetting/entity/NotificationSetting.java index 99683732..09d8499d 100644 --- a/backend/src/main/java/org/example/backend/domain/notificationSetting/entity/NotificationSetting.java +++ b/backend/src/main/java/org/example/backend/domain/notificationSetting/entity/NotificationSetting.java @@ -16,8 +16,6 @@ public class NotificationSetting extends BaseEntity { @Column(name = "user_id") private String userId; - @Column(name = "token", nullable = false, unique = true, length = 512) - private String token; @Column(name = "quiz_upload", nullable = false) @Builder.Default diff --git a/backend/src/main/java/org/example/backend/domain/notificationSetting/service/FcmService.java b/backend/src/main/java/org/example/backend/domain/notificationSetting/service/FcmService.java new file mode 100644 index 00000000..a3cfa912 --- /dev/null +++ b/backend/src/main/java/org/example/backend/domain/notificationSetting/service/FcmService.java @@ -0,0 +1,28 @@ +package org.example.backend.domain.notificationSetting.service; + +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.Notification; +import org.springframework.stereotype.Service; + +@Service +public class FcmService { + + public void sendNotification(String fcmToken, String title, String body) { + try { + Message message = Message.builder() + .setToken(fcmToken) + .setNotification(Notification.builder() + .setTitle(title) + .setBody(body) + .build()) + .build(); + + String response = FirebaseMessaging.getInstance().send(message); + System.out.println("✅ Sent message: " + response); + + } catch (Exception e) { + System.err.println("❌ Failed to send FCM message: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/org/example/backend/domain/notificationSetting/service/NotificationTemplateService.java b/backend/src/main/java/org/example/backend/domain/notificationSetting/service/NotificationTemplateService.java new file mode 100644 index 00000000..18bd859e --- /dev/null +++ b/backend/src/main/java/org/example/backend/domain/notificationSetting/service/NotificationTemplateService.java @@ -0,0 +1,28 @@ +package org.example.backend.domain.notificationSetting.service; + +import org.example.backend.domain.notification.entity.AlarmType; +import org.springframework.stereotype.Service; + +@Service +public class NotificationTemplateService { + + public String getTitle(AlarmType type) { + return switch (type) { + case quizUpload -> "📘 새 퀴즈 업로드"; + case quizAnswerUpload -> "✍️ 퀴즈 답안 업로드"; + case lectureNoteUpload -> "📄 강의 노트 업로드"; + case startLecture -> "📢 강의 시작 알림"; + case recordUpload -> "🎙️ 녹음 파일 업로드"; + }; + } + + public String getBody(AlarmType type, String senderName, String extra) { + return switch (type) { + case quizUpload -> senderName + " 선생님이 퀴즈를 올리셨습니다: " + extra; + case quizAnswerUpload -> senderName + " 선생님이 퀴즈 답안을 업로드하셨습니다."; + case lectureNoteUpload -> senderName + " 선생님이 강의 노트를 공유하셨습니다."; + case startLecture -> senderName + " 선생님의 강의가 곧 시작됩니다. " + extra; + case recordUpload -> senderName + " 선생님이 강의 녹음을 업로드하셨습니다."; + }; + } +} diff --git a/backend/src/main/java/org/example/backend/domain/quiz/service/QuizServiceImpl.java b/backend/src/main/java/org/example/backend/domain/quiz/service/QuizServiceImpl.java index c0880947..9c061d14 100644 --- a/backend/src/main/java/org/example/backend/domain/quiz/service/QuizServiceImpl.java +++ b/backend/src/main/java/org/example/backend/domain/quiz/service/QuizServiceImpl.java @@ -7,6 +7,8 @@ import org.example.backend.domain.lectureNote.repository.LectureNoteRepository; import org.example.backend.domain.lectureNoteMapping.entity.LectureNoteMapping; import org.example.backend.domain.lectureNoteMapping.repository.LectureNoteMappingRepository; +import org.example.backend.domain.notification.entity.AlarmType; +import org.example.backend.domain.notification.service.NotificationService; import org.example.backend.domain.option.entity.Option; import org.example.backend.domain.option.repository.OptionRepository; import org.example.backend.domain.quiz.converter.QuizConverter; @@ -26,9 +28,14 @@ import org.example.backend.global.security.auth.CustomSecurityUtil; import org.example.backend.infra.langchain.LangChainClient; import org.example.backend.global.S3.service.S3Service; +import org.springframework.scheduling.TaskScheduler; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -50,6 +57,8 @@ public class QuizServiceImpl implements QuizService { private final CustomSecurityUtil customSecurityUtil; private final QuizConverter quizConverter; + private final TaskScheduler taskScheduler; + private final NotificationService notificationService; // 퀴즈 생성 및 재생성 @Override @@ -172,6 +181,8 @@ public QuizSaveResponseDTO saveQuiz(UUID lectureId, QuizSaveRequestDTO request) } } } + scheduleQuizAnswerUploadNotification(lecture); + return QuizSaveResponseDTO.builder() .lectureId(lectureId) @@ -179,6 +190,25 @@ public QuizSaveResponseDTO saveQuiz(UUID lectureId, QuizSaveRequestDTO request) .quizIds(savedQuizIds) .build(); } + private void scheduleQuizAnswerUploadNotification(Lecture lecture) { + // 현재 시간 기준으로 "오늘 밤 12시(자정)" 계산 + LocalDateTime midnight = LocalDate.now() + .plusDays(1) // 내일 0시 (오늘 밤 12시) + .atStartOfDay(); + + ZoneId zone = ZoneId.systemDefault(); + Instant triggerTime = midnight.atZone(zone).toInstant(); + + taskScheduler.schedule(() -> { + notificationService.sendAlarmToProfessor( + lecture.getId(), + AlarmType.quizAnswerUpload, + "시스템", + lecture.getLectureName() + " 퀴즈 대시보드가 업로드 되었습니다." + ); + }, triggerTime); + } + // 퀴즈 문제 조회 @Override diff --git a/backend/src/main/java/org/example/backend/global/userdeviceToken/FCMConfig.java b/backend/src/main/java/org/example/backend/global/userdeviceToken/FCMConfig.java new file mode 100644 index 00000000..fef3997a --- /dev/null +++ b/backend/src/main/java/org/example/backend/global/userdeviceToken/FCMConfig.java @@ -0,0 +1,30 @@ +package org.example.backend.global.userdeviceToken; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.FileInputStream; +import java.io.IOException; + + +@Configuration +public class FCMConfig { + @Bean + public FirebaseApp firebaseApp() throws IOException { + if (FirebaseApp.getApps().isEmpty()) { + // 서비스 계정 키 JSON 파일 경로 + FileInputStream serviceAccount = + new FileInputStream("src/main/resources/claog-1e23b-firebase-adminsdk-fbsvc-25a8b72901.json"); + + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(serviceAccount)) + .build(); + + return FirebaseApp.initializeApp(options); + } + return FirebaseApp.getInstance(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/org/example/backend/global/userdeviceToken/controller/UserDeviceTokenController.java b/backend/src/main/java/org/example/backend/global/userdeviceToken/controller/UserDeviceTokenController.java new file mode 100644 index 00000000..724a1ae4 --- /dev/null +++ b/backend/src/main/java/org/example/backend/global/userdeviceToken/controller/UserDeviceTokenController.java @@ -0,0 +1,34 @@ +package org.example.backend.global.userdeviceToken.controller; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.example.backend.global.security.auth.CustomSecurityUtil; +import org.example.backend.global.security.auth.CustomUserDetails; +import org.example.backend.global.userdeviceToken.dto.request.TokenRegisterRequest; +import org.example.backend.global.userdeviceToken.service.UserDeviceTokenService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.UUID; + +@RestController +@RequestMapping("/api/push") +@RequiredArgsConstructor +public class UserDeviceTokenController { + + private final UserDeviceTokenService tokenService; + private final CustomSecurityUtil customSecurityUtil; + + @PostMapping("/register") + public ResponseEntity registerToken(@RequestBody TokenRegisterRequest dto, + @AuthenticationPrincipal CustomUserDetails userDetails) { + UUID userId = customSecurityUtil.getUserId(); + tokenService.registerToken(userId, dto.getToken()); + return ResponseEntity.ok("Token registered successfully"); + } +} \ No newline at end of file diff --git a/backend/src/main/java/org/example/backend/global/userdeviceToken/dto/request/TokenRegisterRequest.java b/backend/src/main/java/org/example/backend/global/userdeviceToken/dto/request/TokenRegisterRequest.java new file mode 100644 index 00000000..6a3d68eb --- /dev/null +++ b/backend/src/main/java/org/example/backend/global/userdeviceToken/dto/request/TokenRegisterRequest.java @@ -0,0 +1,10 @@ +package org.example.backend.global.userdeviceToken.dto.request; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TokenRegisterRequest { + private String token; +} \ No newline at end of file diff --git a/backend/src/main/java/org/example/backend/global/userdeviceToken/entity/UserDeviceToken.java b/backend/src/main/java/org/example/backend/global/userdeviceToken/entity/UserDeviceToken.java new file mode 100644 index 00000000..96060280 --- /dev/null +++ b/backend/src/main/java/org/example/backend/global/userdeviceToken/entity/UserDeviceToken.java @@ -0,0 +1,34 @@ +package org.example.backend.global.userdeviceToken.entity; + +import jakarta.persistence.*; +import lombok.*; +import org.example.backend.global.entitiy.BaseEntity; +import org.example.backend.domain.user.entity.User; + +@Entity +@Table(name = "user_device_token", + uniqueConstraints = { + @UniqueConstraint(name = "uniq_token", columnNames = {"fcm_token"}) + }) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UserDeviceToken extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(name = "fcm_token", nullable = false, length = 512) + private String fcmToken; + + @Builder.Default + @Column(name = "is_active", nullable = false) + private boolean isActive = true; +} \ No newline at end of file diff --git a/backend/src/main/java/org/example/backend/global/userdeviceToken/repository/UserDeviceTokenRepository.java b/backend/src/main/java/org/example/backend/global/userdeviceToken/repository/UserDeviceTokenRepository.java new file mode 100644 index 00000000..8669dcc0 --- /dev/null +++ b/backend/src/main/java/org/example/backend/global/userdeviceToken/repository/UserDeviceTokenRepository.java @@ -0,0 +1,14 @@ +package org.example.backend.global.userdeviceToken.repository; + +import org.example.backend.global.userdeviceToken.entity.UserDeviceToken; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface UserDeviceTokenRepository extends JpaRepository { + Optional findByFcmToken(String token); + + List findAllByUserIdAndIsActiveTrue(UUID userId); +} \ No newline at end of file diff --git a/backend/src/main/java/org/example/backend/global/userdeviceToken/service/UserDeviceTokenService.java b/backend/src/main/java/org/example/backend/global/userdeviceToken/service/UserDeviceTokenService.java new file mode 100644 index 00000000..ad3d6f55 --- /dev/null +++ b/backend/src/main/java/org/example/backend/global/userdeviceToken/service/UserDeviceTokenService.java @@ -0,0 +1,39 @@ +package org.example.backend.global.userdeviceToken.service; + +import lombok.RequiredArgsConstructor; +import org.example.backend.domain.user.entity.User; +import org.example.backend.domain.user.repository.UserRepository; +import org.example.backend.global.userdeviceToken.entity.UserDeviceToken; +import org.example.backend.global.userdeviceToken.repository.UserDeviceTokenRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class UserDeviceTokenService { + + private final UserDeviceTokenRepository repository; + private final UserRepository userRepository; + + @Transactional + public void registerToken(UUID userId, String token) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("User not found")); + + // 이미 등록된 토큰이면 업데이트 + UserDeviceToken existing = repository.findByFcmToken(token).orElse(null); + if (existing != null) { + existing.setUser(user); + existing.setActive(true); + } else { + UserDeviceToken newToken = UserDeviceToken.builder() + .user(user) + .fcmToken(token) + .isActive(true) + .build(); + repository.save(newToken); + } + } +} \ No newline at end of file diff --git a/frontend/api/notifications/fcm.ts b/frontend/api/notifications/fcm.ts new file mode 100644 index 00000000..c8feffee --- /dev/null +++ b/frontend/api/notifications/fcm.ts @@ -0,0 +1,29 @@ +// api/notifications/fcm.ts +import { getToken } from "firebase/messaging"; +import { getFirebaseMessaging } from "@/config/firebase"; +import { axiosInstance } from "@/api/axiosInstance"; +import { ENDPOINTS } from "@/constants/endpoints"; + +const VAPID_KEY = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY!; + +export async function registerFcmToken() { + const messaging = getFirebaseMessaging(); + if (!messaging) return; + + try { + const token = await getToken(messaging, { vapidKey: VAPID_KEY }); + if (token) { + console.log("✅ FCM Token 발급:", token); + + await axiosInstance.post(ENDPOINTS.NOTIFICATIONS.REGISTER_FCM_TOKEN, { + token + }); + + console.log("✅ 서버에 토큰 등록 성공"); + } else { + console.warn("⚠️ FCM 토큰이 없습니다 (사용자가 알림 권한 거부)"); + } + } catch (err) { + console.error("❌ FCM 토큰 등록 실패", err); + } +} \ No newline at end of file diff --git a/frontend/api/notifications/fetchNotification.ts b/frontend/api/notifications/fetchNotification.ts index db195cab..fb82d6cc 100644 --- a/frontend/api/notifications/fetchNotification.ts +++ b/frontend/api/notifications/fetchNotification.ts @@ -15,7 +15,7 @@ const alarmTypeLabels: Record = { quizUpload: "새 퀴즈가 업로드되었습니다", quizAnswerUpload: "퀴즈 답안이 업로드되었습니다", lectureNoteUpload: "새 강의자료가 올라왔습니다", - startLecture: "강의가 시작되었습니다", + startLecture: "강의가 10분 후 시작됩니다.", recordUpload: "녹음 파일이 업로드되었습니다", }; diff --git a/frontend/app/teacher/notification/page.tsx b/frontend/app/teacher/notification/page.tsx index 14b96274..3468b9f9 100644 --- a/frontend/app/teacher/notification/page.tsx +++ b/frontend/app/teacher/notification/page.tsx @@ -55,10 +55,6 @@ export default function TeacherNotificationPage() { {formattedDate} {formattedTime} -
); })} diff --git a/frontend/config/firebase.ts b/frontend/config/firebase.ts new file mode 100644 index 00000000..33431b63 --- /dev/null +++ b/frontend/config/firebase.ts @@ -0,0 +1,30 @@ +// src/firebase.ts +import { initializeApp } from "firebase/app"; +import { getMessaging } from "firebase/messaging"; +import { Messaging } from "firebase/messaging"; + + +const firebaseConfig = { + apiKey: "AIzaSyCR4eTgzcftk7X3NsJsYxZsQGGABAwPbS0", + authDomain: "claog-1e23b.firebaseapp.com", + projectId: "claog-1e23b", + storageBucket: "claog-1e23b.firebasestorage.app", + messagingSenderId: "489862012918", + appId: "1:489862012918:web:152e04239532f7674731d7", + measurementId: "G-E5J4K9Q5F0" +}; + +const app = initializeApp(firebaseConfig); + +// 브라우저 환경에서만 messaging 가져오기 +export function getFirebaseMessaging(): Messaging | null { + if (typeof window !== "undefined" && typeof navigator !== "undefined") { + try { + return getMessaging(app); + } catch (err) { + console.warn("FCM 초기화 실패:", err); + return null; + } + } + return null; +} \ No newline at end of file diff --git a/frontend/constants/endpoints.ts b/frontend/constants/endpoints.ts index 2e4f0f91..d8e32d5d 100644 --- a/frontend/constants/endpoints.ts +++ b/frontend/constants/endpoints.ts @@ -31,6 +31,7 @@ export const ENDPOINTS = { UPDATE_SETTINGS: `${BASE_API}/notifications/setting`, LIST: `${BASE_API}/notifications`, UPDATE_READ_STATUS: `${BASE_API}/notifications`, + REGISTER_FCM_TOKEN: `${BASE_API}/push/register`, }, // 클래스 관련 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a2f74f68..9a12f136 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "dayjs": "^1.11.13", + "firebase": "^12.3.0", "flatpickr": "^4.6.13", "jsonwebtoken": "^9.0.2", "jsqr": "^1.4.0", @@ -1901,6 +1902,614 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@firebase/ai": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.3.0.tgz", + "integrity": "sha512-rVZgf4FszXPSFVIeWLE8ruLU2JDmPXw4XgghcC0x/lK9veGJIyu+DvyumjreVhW/RwD3E5cNPWxQunzylhf/6w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.18", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.18.tgz", + "integrity": "sha512-iN7IgLvM06iFk8BeFoWqvVpRFW3Z70f+Qe2PfCJ7vPIgLPjHXDE774DhCT5Y2/ZU/ZbXPDPD60x/XPWEoZLNdg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.24", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.24.tgz", + "integrity": "sha512-jE+kJnPG86XSqGQGhXXYt1tpTbCTED8OQJ/PQ90SEw14CuxRxx/H+lFbWA1rlFtFSsTCptAJtgyRBwr/f00vsw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.18", + "@firebase/analytics-types": "0.8.3", + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz", + "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.3.tgz", + "integrity": "sha512-by1leTfZkwGycPKRWpc+p5/IhpnOj8zaScVi4RRm9fMoFYS3IE87Wzx1Yf/ruVYowXOEuLqYY3VmJw5tU3+0Bg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.11.0.tgz", + "integrity": "sha512-XAvALQayUMBJo58U/rxW02IhsesaxxfWVmVkauZvGEz3vOAjMEQnzFlyblqkc2iAaO82uJ2ZVyZv9XzPfxjJ6w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.0.tgz", + "integrity": "sha512-UfK2Q8RJNjYM/8MFORltZRG9lJj11k0nW84rrffiKvcJxLf1jf6IEjCIkCamykHE73C6BwqhVfhIBs69GXQV0g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.11.0", + "@firebase/app-check-types": "0.5.3", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz", + "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.3.tgz", + "integrity": "sha512-rRK9YOvgsAU/+edjgubL1q1FyCMjBZZs+fAWtD36tklawkh6WZV07sNLVSceuni+a21oby6xoad+3R8dfztOrA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.14.3", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.11.0.tgz", + "integrity": "sha512-5j7+ua93X+IRcJ1oMDTClTo85l7Xe40WSkoJ+shzPrX7OISlVWLdE1mKC57PSD+/LfAbdhJmvKixINBw2ESK6w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.0.tgz", + "integrity": "sha512-J0lGSxXlG/lYVi45wbpPhcWiWUMXevY4fvLZsN1GHh+po7TZVng+figdHBVhFheaiipU8HZyc7ljw1jNojM2nw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.11.0", + "@firebase/auth-types": "0.13.0", + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz", + "integrity": "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.0.tgz", + "integrity": "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/data-connect": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.11.tgz", + "integrity": "sha512-G258eLzAD6im9Bsw+Qm1Z+P4x0PGNQ45yeUuuqe5M9B1rn0RJvvsQCRHXgE52Z+n9+WX1OJd/crcuunvOGc7Vw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.0.tgz", + "integrity": "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.0.tgz", + "integrity": "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/database": "1.1.0", + "@firebase/database-types": "1.0.16", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.16.tgz", + "integrity": "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.13.0" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.9.2.tgz", + "integrity": "sha512-iuA5+nVr/IV/Thm0Luoqf2mERUvK9g791FZpUJV1ZGXO6RL2/i/WFJUj5ZTVXy5pRjpWYO+ZzPcReNrlilmztA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "@firebase/webchannel-wrapper": "1.0.5", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.2.tgz", + "integrity": "sha512-cy7ov6SpFBx+PHwFdOOjbI7kH00uNKmIFurAn560WiPCZXy9EMnil1SOG7VF4hHZKdenC+AHtL4r3fNpirpm0w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/firestore": "4.9.2", + "@firebase/firestore-types": "3.0.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz", + "integrity": "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.1.tgz", + "integrity": "sha512-sUeWSb0rw5T+6wuV2o9XNmh9yHxjFI9zVGFnjFi+n7drTEWpl7ZTz1nROgGrSu472r+LAaj+2YaSicD4R8wfbw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.1.tgz", + "integrity": "sha512-AxxUBXKuPrWaVNQ8o1cG1GaCAtXT8a0eaTDfqgS5VsRYLAR0ALcfqDLwo/QyijZj1w8Qf8n3Qrfy/+Im245hOQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/functions": "0.13.1", + "@firebase/functions-types": "0.6.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz", + "integrity": "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/installations": { + "version": "0.6.19", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.19.tgz", + "integrity": "sha512-nGDmiwKLI1lerhwfwSHvMR9RZuIH5/8E3kgUWnVRqqL7kGVSktjLTWEMva7oh5yxQ3zXfIlIwJwMcaM5bK5j8Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.19.tgz", + "integrity": "sha512-khfzIY3EI5LePePo7vT19/VEIH1E3iYsHknI/6ek9T8QCozAZshWT9CjlwOzZrKvTHMeNcbpo/VSOSIWDSjWdQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/installations-types": "0.5.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz", + "integrity": "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz", + "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.23", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.23.tgz", + "integrity": "sha512-cfuzv47XxqW4HH/OcR5rM+AlQd1xL/VhuaeW/wzMW1LFrsFcTn0GND/hak1vkQc2th8UisBcrkVcQAnOnKwYxg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.23.tgz", + "integrity": "sha512-SN857v/kBUvlQ9X/UjAqBoQ2FEaL1ZozpnmL1ByTe57iXkmnVVFm9KqAsTfmf+OEwWI4kJJe9NObtN/w22lUgg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/messaging": "0.12.23", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz", + "integrity": "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/performance": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.9.tgz", + "integrity": "sha512-UzybENl1EdM2I1sjYm74xGt/0JzRnU/0VmfMAKo2LSpHJzaj77FCLZXmYQ4oOuE+Pxtt8Wy2BVJEENiZkaZAzQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.22", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.22.tgz", + "integrity": "sha512-xLKxaSAl/FVi10wDX/CHIYEUP13jXUjinL+UaNXT9ByIvxII5Ne5150mx6IgM8G6Q3V+sPiw9C8/kygkyHUVxg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/performance": "0.7.9", + "@firebase/performance-types": "0.2.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz", + "integrity": "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.7.0.tgz", + "integrity": "sha512-dX95X6WlW7QlgNd7aaGdjAIZUiQkgWgNS+aKNu4Wv92H1T8Ue/NDUjZHd9xb8fHxLXIHNZeco9/qbZzr500MjQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.20.tgz", + "integrity": "sha512-P/ULS9vU35EL9maG7xp66uljkZgcPMQOxLj3Zx2F289baTKSInE6+YIkgHEi1TwHoddC/AFePXPpshPlEFkbgg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/remote-config": "0.7.0", + "@firebase/remote-config-types": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.5.0.tgz", + "integrity": "sha512-vI3bqLoF14L/GchtgayMiFpZJF+Ao3uR8WCde0XpYNkSokDpAKca2DxvcfeZv7lZUqkUwQPL2wD83d3vQ4vvrg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.0.tgz", + "integrity": "sha512-xWWbb15o6/pWEw8H01UQ1dC5U3rf8QTAzOChYyCpafV6Xki7KVp3Yaw2nSklUwHEziSWE9KoZJS7iYeyqWnYFA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.0.tgz", + "integrity": "sha512-vDzhgGczr1OfcOy285YAPur5pWDEvD67w4thyeCUh6Ys0izN9fNYtA1MJERmNBfqjqu0lg0FM5GLbw0Il21M+g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/storage": "0.14.0", + "@firebase/storage-types": "0.8.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz", + "integrity": "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.13.0.tgz", + "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.5.tgz", + "integrity": "sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw==", + "license": "Apache-2.0" + }, "node_modules/@floating-ui/core": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz", @@ -1954,6 +2563,37 @@ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", + "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -3182,6 +3822,70 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -4299,6 +5003,12 @@ } }, "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", @@ -4962,6 +5672,20 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -6498,6 +7222,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/firebase": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.3.0.tgz", + "integrity": "sha512-/JVja0IDO8zPETGv4TvvBwo7RwcQFz+RQ3JBETNtUSeqsDdI9G7fhRTkCy1sPKnLzW0xpm/kL8GOj6ncndTT3g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "2.3.0", + "@firebase/analytics": "0.10.18", + "@firebase/analytics-compat": "0.2.24", + "@firebase/app": "0.14.3", + "@firebase/app-check": "0.11.0", + "@firebase/app-check-compat": "0.4.0", + "@firebase/app-compat": "0.5.3", + "@firebase/app-types": "0.9.3", + "@firebase/auth": "1.11.0", + "@firebase/auth-compat": "0.6.0", + "@firebase/data-connect": "0.3.11", + "@firebase/database": "1.1.0", + "@firebase/database-compat": "2.1.0", + "@firebase/firestore": "4.9.2", + "@firebase/firestore-compat": "0.4.2", + "@firebase/functions": "0.13.1", + "@firebase/functions-compat": "0.4.1", + "@firebase/installations": "0.6.19", + "@firebase/installations-compat": "0.2.19", + "@firebase/messaging": "0.12.23", + "@firebase/messaging-compat": "0.2.23", + "@firebase/performance": "0.7.9", + "@firebase/performance-compat": "0.2.22", + "@firebase/remote-config": "0.7.0", + "@firebase/remote-config-compat": "0.2.20", + "@firebase/storage": "0.14.0", + "@firebase/storage-compat": "0.4.0", + "@firebase/util": "1.13.0" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -6674,6 +7434,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -8039,7 +8808,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "dev": true, "license": "MIT" }, "node_modules/lodash.debounce": { @@ -8103,6 +8871,12 @@ "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -9204,6 +9978,30 @@ "react-is": "^16.13.1" } }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -9630,6 +10428,15 @@ "node": ">=6" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -10344,6 +11151,9 @@ "license": "MIT" }, "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", @@ -10377,6 +11187,12 @@ "node": ">=8" } }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/string-width-cjs/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -10531,6 +11347,21 @@ } }, "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", @@ -11675,6 +12506,12 @@ "node": ">=10.13.0" } }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", @@ -12252,6 +13089,9 @@ } }, "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", @@ -12288,7 +13128,7 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { +9 "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", @@ -12352,6 +13192,15 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -12368,6 +13217,33 @@ "node": ">= 6" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 513d503a..a3dd4e6f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "dayjs": "^1.11.13", + "firebase": "^12.3.0", "flatpickr": "^4.6.13", "jsonwebtoken": "^9.0.2", "jsqr": "^1.4.0", diff --git a/frontend/public/firebase-messaging-sw.js b/frontend/public/firebase-messaging-sw.js new file mode 100644 index 00000000..5c5d0dcc --- /dev/null +++ b/frontend/public/firebase-messaging-sw.js @@ -0,0 +1,17 @@ +// Firebase SDK import (compat 버전, 서비스워커는 importScripts 방식) +importScripts("https://www.gstatic.com/firebasejs/9.6.11/firebase-app-compat.js"); +importScripts("https://www.gstatic.com/firebasejs/9.6.11/firebase-messaging-compat.js"); + +// Firebase 초기화 (환경변수 값 대신 실제 Firebase 설정을 그대로 작성) +firebase.initializeApp({ + apiKey: "AIzaSyCR4eTgzcftk7X3NsJsYxZsQGGABAwPbS0", + authDomain: "claog-1e23b.firebaseapp.com", + projectId: "claog-1e23b", + storageBucket: "claog-1e23b.firebasestorage.app", + messagingSenderId: "489862012918", + appId: "1:489862012918:web:152e04239532f7674731d7", + measurementId: "G-E5J4K9Q5F0" +}); + +// Firebase Messaging 인스턴스 (자동으로 알림 표시 처리) +firebase.messaging(); diff --git a/frontend/store/useAuthStore.ts b/frontend/store/useAuthStore.ts index 816ffe9c..ecceca74 100644 --- a/frontend/store/useAuthStore.ts +++ b/frontend/store/useAuthStore.ts @@ -6,6 +6,7 @@ import useLectureListStore from "./useLectureListStore"; import useSelectedClassStore from "./useSelectedClassStore"; import useClassListStore from "./useClassListStore"; import { useSignupStore } from "./useSignupStore"; +import {registerFcmToken} from "@/api/notifications/fcm"; import { useLectureStatusStore } from "./useLectureStatusStore"; import { useClassTitleStore } from "./useClassTitleStore"; @@ -103,6 +104,7 @@ export const useAuthStore = create((set, get) => ({ }); // localStorage에도 저장 localStorage.setItem("accessToken", token); + registerFcmToken(); // 기존 타이머가 있으면 제거 if (refreshTimeout) clearTimeout(refreshTimeout);