Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import org.example.backend.domain.classroom.entity.Classroom;
import org.example.backend.domain.lectureNoteMapping.entity.LectureNoteMapping;
import org.example.backend.domain.notification.entity.Notification;
import org.example.backend.domain.question.entity.Question;
import org.example.backend.domain.quiz.entity.Quiz;
import org.example.backend.domain.studentClass.entity.StudentClass;
Expand Down Expand Up @@ -65,4 +66,7 @@ public class Lecture extends BaseEntity {
@OneToMany(mappedBy = "lecture", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Question> questions = new ArrayList<>();

@OneToMany(mappedBy = "lecture", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Notification> notification = new ArrayList<>();

}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ public void notifyProfessorBeforeLecture() {
"시스템",
lecture.getLectureName() + " 강의가 10분 후 시작됩니다."
);

notificationService.sendAlarmToAllStudentsInLecture(
lecture.getId(),
AlarmType.startLecture,
"시스템",
lecture.getLectureName() + " 수업이 10분 후 시작됩니다."
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
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.service.NotificationService;
import org.example.backend.global.S3.service.S3Service;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.example.backend.domain.notification.entity.AlarmType;

import java.io.IOException;
import java.util.ArrayList;
Expand All @@ -29,22 +31,24 @@ public class LectureNoteServiceImpl implements LectureNoteService {

private final S3Service s3Service;
private final LectureNoteRepository lectureNoteRepository;
private final ClassroomRepository classroomRepository; // ✅ 추가
private final ClassroomRepository classroomRepository;
private final LectureNoteMappingRepository lectureNoteMappingRepository;
private final NotificationService notificationService;

public List<LectureNote> uploadLectureNotes(UUID classId, List<MultipartFile> files) throws IOException {
// 1. 여러 파일에 대해 처리
List<LectureNote> lectureNotes = new ArrayList<>();

// 2) classId로 Classroom 엔티티 조회
Classroom classroom = classroomRepository.findById(classId)
.orElseThrow(() -> new ClassroomException(ClassroomErrorCode.CLASS_NOT_FOUND));

// 2. 각 파일 업로드 처리
for (MultipartFile file : files) {
// 1) S3에 업로드
String key = "lecture_note/" + classId + "/" + UUID.randomUUID() + "/" + file.getOriginalFilename();
String fileUrl = s3Service.uploadFile(file, key);

// 2) classId로 Classroom 엔티티 조회
Classroom classroom = classroomRepository.findById(classId)
.orElseThrow(() -> new ClassroomException(ClassroomErrorCode.CLASS_NOT_FOUND));

// 3) LectureNote 객체 생성
LectureNote lectureNote = LectureNote.builder()
Expand All @@ -56,6 +60,13 @@ public List<LectureNote> uploadLectureNotes(UUID classId, List<MultipartFile> fi
lectureNotes.add(lectureNoteRepository.save(lectureNote));
}

notificationService.sendAlarmToAllStudentsInClass(
classId,
AlarmType.lectureNoteUpload, // enum에 없다면 추가 필요
"시스템",
classroom.getClassName() + " 강의자료가 업로드되었습니다."
);

return lectureNotes;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ public class Notification extends BaseEntity {
@JoinColumn(name = "user_id", nullable = false)
private User user;

@ManyToOne
@JoinColumn(name = "lecture_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY, optional = true)
@JoinColumn(name = "lecture_id", nullable = true)
private Lecture lecture;

@Enumerated(EnumType.STRING)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.example.backend.domain.notification.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.backend.domain.classroom.entity.Classroom;
import org.example.backend.domain.classroom.repository.ClassroomRepository;
import org.example.backend.domain.lecture.entity.Lecture;
Expand All @@ -12,13 +13,17 @@
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.domain.studentClass.repository.StudentClassRepository;
import org.example.backend.domain.user.entity.User;
import org.example.backend.domain.user.repository.UserRepository;
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;

@Slf4j
@RequiredArgsConstructor
@Service
public class NotificationService implements NotificationServiceImpl{
Expand All @@ -29,6 +34,8 @@ public class NotificationService implements NotificationServiceImpl{
private final NotificationTemplateService templateService;
private final UserDeviceTokenRepository tokenRepository;
private final FcmService fcmService;
private final StudentClassRepository studentClassRepository;
private final UserRepository userRepository;

public List<NotificationResponseDTO> getNotificationsByUserId(UUID userId) {
List<Notification> notificationList =
Expand Down Expand Up @@ -71,6 +78,137 @@ public void sendAlarmToProfessor(UUID lectureId, AlarmType type, String senderNa
notificationRepository.save(notification);
}

@Transactional
public void sendAlarmToStudentInLecture(UUID lectureId, UUID studentUserId,
AlarmType type, String senderName, String extra) {
Lecture lecture = lectureRepository.findById(lectureId)
.orElseThrow(() -> new RuntimeException("Lecture not found"));

UUID classId = lecture.getClassroom().getId();

// 수강 검증 (해당 반의 학생인지)
if (!studentClassRepository.existsByClassIdAndUserId(classId, studentUserId)) {
throw new RuntimeException("Student does not belong to this class");
}

String title = templateService.getTitle(type);
String body = templateService.getBody(type, senderName, extra);

sendToUser(studentUserId, title, body);

User student = userRepository.findById(studentUserId)
.orElseThrow(() -> new RuntimeException("User not found"));
Notification notification = Notification.builder()
.user(student)
.lecture(lecture)
.alarmType(type)
.isRead(false)
.build();
notificationRepository.save(notification);
}

@Transactional
public void sendAlarmToAllStudentsInLecture(UUID lectureId,
AlarmType type, String senderName, String extra) {
Lecture lecture = lectureRepository.findById(lectureId)
.orElseThrow(() -> new RuntimeException("Lecture not found"));

UUID classId = lecture.getClassroom().getId();

String title = templateService.getTitle(type);
String body = templateService.getBody(type, senderName, extra);

// 이 반에 속한 모든 학생 userId
List<UUID> studentIds = studentClassRepository.findUserIdsByClassId(classId);

log.info("[Notification] classId={}, lectureId={}, studentCount={}", classId, lectureId, studentIds);
log.debug("[Notification] studentIds={}", studentIds);

// (간단 버전) 각 유저별 토큰 찾아 전송
studentIds.stream().distinct().forEach(id -> sendToUser(id, title, body));

List<Notification> notifications = studentIds.stream()
.distinct()
.map(id -> Notification.builder()
.user(userRepository.getReferenceById(id))
.lecture(lecture)
.alarmType(type)
.isRead(false)
.build()
).toList();

notificationRepository.saveAll(notifications);
}

/** 공통 전송 헬퍼: 유저의 활성 FCM 토큰 전부에 발송 */
private void sendToUser(UUID userId, String title, String body) {
var tokens = tokenRepository.findAllByUserIdAndIsActiveTrue(userId);
tokens.forEach(token -> fcmService.sendNotification(token.getFcmToken(), title, body));
}
@Transactional
public void sendAlarmToAllStudentsInClass(UUID classId,
AlarmType type,
String senderName,
String extra) {
// 1) 반 로딩
Classroom classroom = classroomRepository.findById(classId)
.orElseThrow(() -> new RuntimeException("Classroom not found"));

// 2) 템플릿 생성
String title = templateService.getTitle(type);
String body = templateService.getBody(type, senderName, extra);

// 3) 학생 userId 조회 (null 제거 + 중복 제거)
List<UUID> studentIds = studentClassRepository.findUserIdsByClassId(classId)
.stream()
.filter(java.util.Objects::nonNull)
.distinct()
.toList();

log.info("[Notification] classId={}, studentCount={}", classId, studentIds.size());
log.debug("[Notification] studentIds={}", studentIds);

if (studentIds.isEmpty()) return;

// 4) 푸시 발송 (유저별 활성 토큰에 전송)
studentIds.forEach(id -> sendToUser(id, title, body));

// 5) Notification 저장 (lecture는 클래스 단위이므로 null 가능해야 함)
var students = userRepository.findAllById(studentIds);
var userMap = students.stream().collect(
java.util.stream.Collectors.toMap(
org.example.backend.domain.user.entity.User::getId,
java.util.function.Function.identity()
)
);

List<Notification> notifications = studentIds.stream()
.map(id -> {
User u = userMap.get(id);
if (u == null) {
log.warn("[Notification] user not found for userId={} (skipped)", id);
return null;
}
return Notification.builder()
.user(u)
.lecture(null) // 특정 회차가 없으므로 null (엔티티가 nullable이어야 함)
.alarmType(type)
.isRead(false)
.build();
})
.filter(java.util.Objects::nonNull)
.toList();

notificationRepository.saveAll(notifications);
log.info("[Notification] saved notifications={}", notifications.size());
}







@Transactional
public void markAllAsRead(UUID userId) {
notificationRepository.markAllAsReadByUserId(userId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import lombok.*;
import org.example.backend.global.entitiy.BaseEntity;

import java.util.UUID;

@Entity
@Table(name = "notification_setting")
@Data
Expand All @@ -14,7 +16,7 @@ public class NotificationSetting extends BaseEntity {

@Id
@Column(name = "user_id")
private String userId;
private UUID userId;


@Column(name = "quiz_upload", nullable = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@
import org.example.backend.domain.notificationSetting.entity.NotificationSetting;
import org.springframework.data.jpa.repository.JpaRepository;

public interface NotificationSettingRepository extends JpaRepository<NotificationSetting, String> {
import java.util.UUID;

public interface NotificationSettingRepository extends JpaRepository<NotificationSetting, UUID> {
boolean existsByUserId(UUID userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ public class NotificationSettingService implements NotificationSettingServiceImp

@Override
public NotificationSettingResponseDTO getNotiSetting(UUID userId) {
NotificationSetting setting = notificationSettingRepository.findById(userId.toString())
NotificationSetting setting = notificationSettingRepository.findById(userId)
.orElseGet(() -> NotificationSetting.builder()
.userId(userId.toString())
.userId(userId)
.quizUpload(true)
.quizAnswerUpload(true)
.lectureNoteUpload(true)
Expand All @@ -32,7 +32,7 @@ public NotificationSettingResponseDTO getNotiSetting(UUID userId) {

@Transactional
public void patchSettings(UUID userId, NotificationSettingPatchRequest req) {
NotificationSetting entity = notificationSettingRepository.findById(userId.toString())
NotificationSetting entity = notificationSettingRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("알림 설정이 존재하지 않습니다."));

if (req.quizUpload() != null) entity.setQuizUpload(req.quizUpload());
Expand All @@ -41,4 +41,20 @@ public void patchSettings(UUID userId, NotificationSettingPatchRequest req) {
if (req.lectureUpload() != null) entity.setLectureUpload(req.lectureUpload());
if (req.recordUpload() != null) entity.setRecordUpload(req.recordUpload());
}

public void initializeDefaultSettings(UUID userId) {
// 이미 존재하는 설정이 있으면 중복 생성 방지
if (notificationSettingRepository.existsByUserId(userId)) return;

NotificationSetting notificationSetting = NotificationSetting.builder()
.userId(userId)
.quizUpload(true)
.quizAnswerUpload(true)
.lectureNoteUpload(true)
.lectureUpload(true)
.recordUpload(true)
.build();

notificationSettingRepository.save(notificationSetting);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ public interface NotificationSettingServiceImpl {
NotificationSettingResponseDTO getNotiSetting(UUID userId);

void patchSettings(UUID userId, NotificationSettingPatchRequest req);
void initializeDefaultSettings(UUID userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,12 @@ public QuizSaveResponseDTO saveQuiz(UUID lectureId, QuizSaveRequestDTO request)
}
}
}
notificationService.sendAlarmToAllStudentsInLecture(
lecture.getId(),
AlarmType.quizUpload,
"시스템",
lecture.getLectureName() + " 퀴즈가 게시되었습니다. 풀어보러 갈까요?."
);
scheduleQuizAnswerUploadNotification(lecture);


Expand All @@ -206,6 +212,13 @@ private void scheduleQuizAnswerUploadNotification(Lecture lecture) {
"시스템",
lecture.getLectureName() + " 퀴즈 대시보드가 업로드 되었습니다."
);

notificationService.sendAlarmToAllStudentsInLecture(
lecture.getId(),
AlarmType.quizAnswerUpload,
"시스템",
lecture.getLectureName() + " 퀴즈 정답이 공개되었습니다."
);
}, triggerTime);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,11 @@ SELECT CASE WHEN COUNT(sc) > 0 THEN TRUE ELSE FALSE END
)
""")
boolean existsByUserIdAndLectureId(@Param("userId") UUID userId, @Param("lectureId") UUID lectureId);

// 해당 클래스에 속한 학생들의 userId만 조회
@Query("select sc.userId from StudentClass sc where sc.classId = :classId")
List<UUID> findUserIdsByClassId(UUID classId);

// 특정 학생이 해당 클래스에 속해있는지 여부
boolean existsByClassIdAndUserId(UUID classId, UUID userId);
}
Loading