Skip to content

Commit f0c9f2e

Browse files
authored
Merge pull request #327 from KW-ClassLog/Feat/#270/noti-setting
✨ Feat/#270 선생님-알림 보내기 구현
2 parents 5724f43 + c9816a2 commit f0c9f2e

26 files changed

Lines changed: 1313 additions & 10 deletions

File tree

backend/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ dependencies {
4747
implementation 'com.amazonaws:aws-java-sdk-core:1.12.681'
4848
implementation 'org.apache.commons:commons-lang3:3.12.0'
4949
implementation 'org.springframework.boot:spring-boot-starter-websocket'
50+
implementation 'com.google.firebase:firebase-admin:9.2.0'
5051
}
5152

5253
tasks.named('test') {

backend/src/main/java/org/example/backend/BackendApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
import org.springframework.boot.SpringApplication;
55
import org.springframework.boot.autoconfigure.SpringBootApplication;
66
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
7+
import org.springframework.scheduling.annotation.EnableScheduling;
78

89
@SpringBootApplication
910
@EnableJpaAuditing
11+
@EnableScheduling
1012
public class BackendApplication {
1113
public static void main(String[] args) {
1214
// .env 파일 로딩 (없어도 실행되게 ignoreIfMissing 추가)

backend/src/main/java/org/example/backend/domain/lecture/repository/LectureRepository.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package org.example.backend.domain.lecture.repository;
22

3+
import io.lettuce.core.dynamic.annotation.Param;
34
import org.example.backend.domain.classroom.entity.Classroom;
45
import org.example.backend.domain.lecture.entity.Lecture;
56
import org.springframework.data.jpa.repository.JpaRepository;
7+
import org.springframework.data.jpa.repository.Query;
68

79
import java.time.LocalDate;
10+
import java.time.LocalTime;
811
import java.util.List;
912
import java.util.UUID;
1013

@@ -15,4 +18,12 @@ public interface LectureRepository extends JpaRepository<Lecture, UUID> {
1518
List<Lecture> findByClassroomInAndLectureDate(List<Classroom> classrooms, LocalDate lectureDate);
1619
List<Lecture> findByClassroom_IdOrderByLectureDateAscStartTimeAsc(UUID classId);
1720

21+
@Query("SELECT l FROM Lecture l " +
22+
"WHERE l.lectureDate = :today " +
23+
"AND l.startTime = :targetTime " +
24+
"AND l.isLectureStart = false")
25+
List<Lecture> findLecturesStartingAt(
26+
@Param("today") LocalDate today,
27+
@Param("targetTime") LocalTime targetTime
28+
);
1829
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.example.backend.domain.lecture.service;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.example.backend.domain.lecture.entity.Lecture;
5+
import org.example.backend.domain.lecture.repository.LectureRepository;
6+
import org.example.backend.domain.notification.entity.AlarmType;
7+
import org.example.backend.domain.notification.service.NotificationService;
8+
import org.springframework.scheduling.annotation.Scheduled;
9+
import org.springframework.stereotype.Component;
10+
11+
import java.time.LocalDate;
12+
import java.time.LocalTime;
13+
import java.util.List;
14+
15+
@Component
16+
@RequiredArgsConstructor
17+
public class LectureScheduler {
18+
19+
private final LectureRepository lectureRepository;
20+
private final NotificationService notificationService;
21+
22+
// 매 분마다 실행
23+
@Scheduled(cron = "0 * * * * *")
24+
public void notifyProfessorBeforeLecture() {
25+
LocalDate today = LocalDate.now();
26+
LocalTime now = LocalTime.now().withSecond(0).withNano(0);
27+
28+
// 🔥 "현재 시각 + 10분"이 lecture start_time 인 강의 찾기
29+
LocalTime targetStartTime = now.plusMinutes(10);
30+
31+
List<Lecture> lectures = lectureRepository.findLecturesStartingAt(today, targetStartTime);
32+
33+
for (Lecture lecture : lectures) {
34+
notificationService.sendAlarmToProfessor(
35+
lecture.getId(),
36+
AlarmType.startLecture,
37+
"시스템",
38+
lecture.getLectureName() + " 강의가 10분 후 시작됩니다."
39+
);
40+
}
41+
}
42+
}

backend/src/main/java/org/example/backend/domain/notification/controller/NotificationController.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.example.backend.global.ApiResponse;
77
import org.example.backend.global.security.auth.CustomSecurityUtil;
88
import org.springframework.web.bind.annotation.GetMapping;
9+
import org.springframework.web.bind.annotation.PatchMapping;
910
import org.springframework.web.bind.annotation.RequestMapping;
1011
import org.springframework.web.bind.annotation.RestController;
1112

@@ -28,4 +29,10 @@ public ApiResponse<List<NotificationResponseDTO>> getNotifications() {
2829
List<NotificationResponseDTO> notifications = notificationService.getNotificationsByUserId(userId);
2930
return ApiResponse.onSuccess(notifications);
3031
}
32+
33+
@PatchMapping("/read-all")
34+
public void markAllAsRead() {
35+
UUID userId = customSecurityUtil.getUserId();
36+
notificationService.markAllAsRead(userId);
37+
}
3138
}

backend/src/main/java/org/example/backend/domain/notification/repository/NotificationRepository.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package org.example.backend.domain.notification.repository;
22

3+
import io.lettuce.core.dynamic.annotation.Param;
34
import org.example.backend.domain.classroom.entity.Classroom;
45
import org.example.backend.domain.notification.entity.Notification;
56
import org.springframework.data.jpa.repository.JpaRepository;
7+
import org.springframework.data.jpa.repository.Modifying;
8+
import org.springframework.data.jpa.repository.Query;
69
import org.springframework.stereotype.Repository;
710

811
import java.util.List;
@@ -11,4 +14,8 @@
1114
@Repository
1215
public interface NotificationRepository extends JpaRepository<Notification, UUID> {
1316
List<Notification> findByUserIdOrderByCreatedAtDesc(UUID userId);
17+
18+
@Modifying
19+
@Query("UPDATE Notification n SET n.isRead = true WHERE n.user.id = :userId")
20+
void markAllAsReadByUserId(@Param("userId") UUID userId);
1421
}

backend/src/main/java/org/example/backend/domain/notification/service/NotificationService.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@
77
import org.example.backend.domain.lecture.repository.LectureRepository;
88
import org.example.backend.domain.notification.converter.NotificationConverter;
99
import org.example.backend.domain.notification.dto.response.NotificationResponseDTO;
10+
import org.example.backend.domain.notification.entity.AlarmType;
1011
import org.example.backend.domain.notification.entity.Notification;
1112
import org.example.backend.domain.notification.repository.NotificationRepository;
13+
import org.example.backend.domain.notificationSetting.service.FcmService;
14+
import org.example.backend.domain.notificationSetting.service.NotificationTemplateService;
15+
import org.example.backend.global.userdeviceToken.repository.UserDeviceTokenRepository;
1216
import org.springframework.stereotype.Service;
17+
import org.springframework.transaction.annotation.Transactional;
1318

1419
import java.util.List;
1520
import java.util.UUID;
@@ -20,7 +25,10 @@ public class NotificationService implements NotificationServiceImpl{
2025
private final NotificationRepository notificationRepository;
2126
private final LectureRepository lectureRepository;
2227
private final ClassroomRepository classroomRepository;
23-
private final NotificationConverter notificationConverter;;
28+
private final NotificationConverter notificationConverter;
29+
private final NotificationTemplateService templateService;
30+
private final UserDeviceTokenRepository tokenRepository;
31+
private final FcmService fcmService;
2432

2533
public List<NotificationResponseDTO> getNotificationsByUserId(UUID userId) {
2634
List<Notification> notificationList =
@@ -39,4 +47,32 @@ public List<NotificationResponseDTO> getNotificationsByUserId(UUID userId) {
3947
})
4048
.toList();
4149
}
50+
51+
public void sendAlarmToProfessor(UUID lectureId, AlarmType type, String senderName, String extra) {
52+
Lecture lecture = lectureRepository.findById(lectureId)
53+
.orElseThrow(() -> new RuntimeException("Lecture not found"));
54+
55+
UUID professorId = lecture.getClassroom().getProfessor().getId();
56+
57+
String title = templateService.getTitle(type);
58+
String body = templateService.getBody(type, senderName, extra);
59+
60+
var tokens = tokenRepository.findAllByUserIdAndIsActiveTrue(professorId);
61+
tokens.forEach(token ->
62+
fcmService.sendNotification(token.getFcmToken(), title, body)
63+
);
64+
65+
Notification notification = Notification.builder()
66+
.user(lecture.getClassroom().getProfessor())
67+
.lecture(lecture)
68+
.alarmType(type)
69+
.isRead(false)
70+
.build();
71+
notificationRepository.save(notification);
72+
}
73+
74+
@Transactional
75+
public void markAllAsRead(UUID userId) {
76+
notificationRepository.markAllAsReadByUserId(userId);
77+
}
4278
}

backend/src/main/java/org/example/backend/domain/notificationSetting/entity/NotificationSetting.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ public class NotificationSetting extends BaseEntity {
1616
@Column(name = "user_id")
1717
private String userId;
1818

19-
@Column(name = "token", nullable = false, unique = true, length = 512)
20-
private String token;
2119

2220
@Column(name = "quiz_upload", nullable = false)
2321
@Builder.Default
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package org.example.backend.domain.notificationSetting.service;
2+
3+
import com.google.firebase.messaging.FirebaseMessaging;
4+
import com.google.firebase.messaging.Message;
5+
import com.google.firebase.messaging.Notification;
6+
import org.springframework.stereotype.Service;
7+
8+
@Service
9+
public class FcmService {
10+
11+
public void sendNotification(String fcmToken, String title, String body) {
12+
try {
13+
Message message = Message.builder()
14+
.setToken(fcmToken)
15+
.setNotification(Notification.builder()
16+
.setTitle(title)
17+
.setBody(body)
18+
.build())
19+
.build();
20+
21+
String response = FirebaseMessaging.getInstance().send(message);
22+
System.out.println("✅ Sent message: " + response);
23+
24+
} catch (Exception e) {
25+
System.err.println("❌ Failed to send FCM message: " + e.getMessage());
26+
}
27+
}
28+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package org.example.backend.domain.notificationSetting.service;
2+
3+
import org.example.backend.domain.notification.entity.AlarmType;
4+
import org.springframework.stereotype.Service;
5+
6+
@Service
7+
public class NotificationTemplateService {
8+
9+
public String getTitle(AlarmType type) {
10+
return switch (type) {
11+
case quizUpload -> "📘 새 퀴즈 업로드";
12+
case quizAnswerUpload -> "✍️ 퀴즈 답안 업로드";
13+
case lectureNoteUpload -> "📄 강의 노트 업로드";
14+
case startLecture -> "📢 강의 시작 알림";
15+
case recordUpload -> "🎙️ 녹음 파일 업로드";
16+
};
17+
}
18+
19+
public String getBody(AlarmType type, String senderName, String extra) {
20+
return switch (type) {
21+
case quizUpload -> senderName + " 선생님이 퀴즈를 올리셨습니다: " + extra;
22+
case quizAnswerUpload -> senderName + " 선생님이 퀴즈 답안을 업로드하셨습니다.";
23+
case lectureNoteUpload -> senderName + " 선생님이 강의 노트를 공유하셨습니다.";
24+
case startLecture -> senderName + " 선생님의 강의가 곧 시작됩니다. " + extra;
25+
case recordUpload -> senderName + " 선생님이 강의 녹음을 업로드하셨습니다.";
26+
};
27+
}
28+
}

0 commit comments

Comments
 (0)