diff --git a/src/main/java/gdsc/binaryho/imhere/config/redis/RedisConfig.java b/src/main/java/gdsc/binaryho/imhere/config/redis/RedisConfig.java index 6ae90bc..d5fb636 100644 --- a/src/main/java/gdsc/binaryho/imhere/config/redis/RedisConfig.java +++ b/src/main/java/gdsc/binaryho/imhere/config/redis/RedisConfig.java @@ -23,7 +23,8 @@ public class RedisConfig { @Bean public RedisConnectionFactory redisConnectionFactory() { - RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(redisHost, redisPort); +// RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(redisHost, redisPort); + RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration("localhost", 6379); configuration.setPassword(redisPassword); return new LettuceConnectionFactory(configuration); diff --git a/src/main/java/gdsc/binaryho/imhere/core/attendance/application/AttendanceFailedEvent.java b/src/main/java/gdsc/binaryho/imhere/core/attendance/application/AttendanceFailedEvent.java new file mode 100644 index 0000000..0e841ac --- /dev/null +++ b/src/main/java/gdsc/binaryho/imhere/core/attendance/application/AttendanceFailedEvent.java @@ -0,0 +1,13 @@ +package gdsc.binaryho.imhere.core.attendance.application; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class AttendanceFailedEvent { + + private final long lectureId; + private final long studentId; + private final Throwable exception; +} diff --git a/src/main/java/gdsc/binaryho/imhere/core/attendance/application/AttendanceHistoryCacheService.java b/src/main/java/gdsc/binaryho/imhere/core/attendance/application/AttendanceHistoryCacheService.java index 23d216f..5d632b8 100644 --- a/src/main/java/gdsc/binaryho/imhere/core/attendance/application/AttendanceHistoryCacheService.java +++ b/src/main/java/gdsc/binaryho/imhere/core/attendance/application/AttendanceHistoryCacheService.java @@ -1,8 +1,13 @@ package gdsc.binaryho.imhere.core.attendance.application; +import static gdsc.binaryho.imhere.core.attendance.domain.AttendanceHistory.createAcceptedAttendanceHistory; +import static gdsc.binaryho.imhere.core.attendance.domain.AttendanceHistory.createAwaitAttendanceHistory; +import static gdsc.binaryho.imhere.core.attendance.domain.AttendanceHistory.createFailedAttendanceHistory; + import gdsc.binaryho.imhere.core.attendance.application.port.AttendanceHistoryCacheRepository; import gdsc.binaryho.imhere.core.attendance.domain.AttendanceHistory; import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; @@ -10,6 +15,7 @@ import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; +@Log4j2 @Service @RequiredArgsConstructor public class AttendanceHistoryCacheService { @@ -19,13 +25,41 @@ public class AttendanceHistoryCacheService { @Async @Transactional(propagation = Propagation.REQUIRES_NEW) @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - public void cache(StudentAttendedEvent event) { + public void cacheAttendanceHistory(AttendanceRequestedEvent event) { long lectureId = event.getLectureId(); long studentId = event.getStudentId(); - String timestamp = event.getTimestamp().toString(); - AttendanceHistory attendanceHistory = AttendanceHistory.of( - lectureId, studentId, timestamp); + AttendanceHistory attendanceHistory = createAwaitAttendanceHistory(lectureId, studentId); attendanceHistoryCacheRepository.cache(attendanceHistory); } + + @Async + @Transactional(propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void saveAttendance(AttendanceSaveSucceedEvent event) { + long lectureId = event.getLectureId(); + long studentId = event.getStudentId(); + + AttendanceHistory attendanceHistory = createAcceptedAttendanceHistory(lectureId, studentId); + attendanceHistoryCacheRepository.cache(attendanceHistory); + } + + @Async + @Transactional(propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void saveAttendance(AttendanceFailedEvent event) { + long lectureId = event.getLectureId(); + long studentId = event.getStudentId(); + + AttendanceHistory attendanceHistory = createFailedAttendanceHistory(lectureId, studentId); + attendanceHistoryCacheRepository.cache(attendanceHistory); + logAttendanceFailed(studentId, lectureId, event.getException()); + } + + private void logAttendanceFailed(Long studentId, Long lectureId, RuntimeException exception) { + String exceptionName = exception.getClass().getName(); + String exceptionMessage = exception.getMessage(); + log.info("[출석 실패] 학생 id {}, 수업 id : {}, 예외 이름 : {}, 예외 메시지 : {}", + studentId, lectureId, exceptionName, exceptionMessage); + } } diff --git a/src/main/java/gdsc/binaryho/imhere/core/attendance/application/AttendanceRequestedEvent.java b/src/main/java/gdsc/binaryho/imhere/core/attendance/application/AttendanceRequestedEvent.java new file mode 100644 index 0000000..020b666 --- /dev/null +++ b/src/main/java/gdsc/binaryho/imhere/core/attendance/application/AttendanceRequestedEvent.java @@ -0,0 +1,12 @@ +package gdsc.binaryho.imhere.core.attendance.application; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class AttendanceRequestedEvent { + + private final long lectureId; + private final long studentId; +} diff --git a/src/main/java/gdsc/binaryho/imhere/core/attendance/application/AttendanceSaveRequestStatus.java b/src/main/java/gdsc/binaryho/imhere/core/attendance/application/AttendanceSaveRequestStatus.java new file mode 100644 index 0000000..7248154 --- /dev/null +++ b/src/main/java/gdsc/binaryho/imhere/core/attendance/application/AttendanceSaveRequestStatus.java @@ -0,0 +1,53 @@ +package gdsc.binaryho.imhere.core.attendance.application; + +import org.springframework.data.redis.core.RedisTemplate; + +public enum AttendanceSaveRequestStatus { + + /* + * 또한, 설문을 통해 유저들이 요청한 “출석 성공 조회”기능을 위해, 출석 요청, 성공, 예외 발생시 이벤트를 발행해 캐싱한다. + * 출석 성공은 다른 상태를 덮어 쓰고, 다른 상태는 출석 성공을 덮어 쓸 수 없다. + * */ + + PROCESSING { + @Override + public void cache( + RedisTemplate redisTemplate, String key, String value) { + String savedValue = redisTemplate.opsForValue().getAndDelete(key); + if (SUCCESS.name().equals(savedValue)) + redisTemplate.opsForValue().set(key, value); + } + }, + + SUCCESS { + @Override + public void cache( + RedisTemplate redisTemplate, String key, String value) { + String savedValue = redisTemplate.opsForValue().getAndDelete(key); + redisTemplate.opsForValue().set(key, savedValue, value); + } + }, + + FAILED { + @Override + public void cache( + RedisTemplate redisTemplate, String key, String value) { + redisTemplate.opsForSet().add(key, value); + } + }, + + NO_REQUEST { + @Override + public void cache( + RedisTemplate redisTemplate, String key, String value) { + redisTemplate.opsForSet().add(key, value); + } + }; + + public abstract void cache( + RedisTemplate redisTemplate, String key, String value); + + public boolean canCache(String originalValue) { + this.name() + } +} diff --git a/src/main/java/gdsc/binaryho/imhere/core/attendance/application/AttendanceSaveService.java b/src/main/java/gdsc/binaryho/imhere/core/attendance/application/AttendanceSaveService.java new file mode 100644 index 0000000..3638a36 --- /dev/null +++ b/src/main/java/gdsc/binaryho/imhere/core/attendance/application/AttendanceSaveService.java @@ -0,0 +1,32 @@ +package gdsc.binaryho.imhere.core.attendance.application; + +import gdsc.binaryho.imhere.core.attendance.domain.Attendance; +import gdsc.binaryho.imhere.core.attendance.infrastructure.AttendanceRepository; +import gdsc.binaryho.imhere.core.lecture.domain.Lecture; +import gdsc.binaryho.imhere.core.member.Member; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Log4j2 +@Service +@RequiredArgsConstructor +public class AttendanceSaveService { + + private final AttendanceRepository attendanceRepository; + + @Transactional + public void save(Attendance attendance) { + attendanceRepository.save(attendance); + logAttendanceHistory(attendance); + } + + private void logAttendanceHistory(Attendance attendance) { + Member student = attendance.getStudent(); + Lecture lecture = attendance.getLecture(); + log.info("[출석 완료] {}({}) , 학생 : {} ({})", + lecture::getLectureName, lecture::getId, + student::getUnivId, student::getName); + } +} diff --git a/src/main/java/gdsc/binaryho/imhere/core/attendance/application/AttendanceSaveSucceedEvent.java b/src/main/java/gdsc/binaryho/imhere/core/attendance/application/AttendanceSaveSucceedEvent.java new file mode 100644 index 0000000..934e9bd --- /dev/null +++ b/src/main/java/gdsc/binaryho/imhere/core/attendance/application/AttendanceSaveSucceedEvent.java @@ -0,0 +1,12 @@ +package gdsc.binaryho.imhere.core.attendance.application; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class AttendanceSaveSucceedEvent { + + private final long lectureId; + private final long studentId; +} diff --git a/src/main/java/gdsc/binaryho/imhere/core/attendance/application/StudentAttendanceService.java b/src/main/java/gdsc/binaryho/imhere/core/attendance/application/StudentAttendanceService.java index 1cc8a40..c3dcd85 100644 --- a/src/main/java/gdsc/binaryho/imhere/core/attendance/application/StudentAttendanceService.java +++ b/src/main/java/gdsc/binaryho/imhere/core/attendance/application/StudentAttendanceService.java @@ -1,9 +1,11 @@ package gdsc.binaryho.imhere.core.attendance.application; -import gdsc.binaryho.imhere.core.attendance.domain.Attendance; +import static gdsc.binaryho.imhere.core.attendance.application.AttendanceSaveRequestStatus.NO_REQUEST; +import static gdsc.binaryho.imhere.core.attendance.application.AttendanceSaveRequestStatus.SUCCESS; + import gdsc.binaryho.imhere.core.attendance.application.port.AttendanceHistoryCacheRepository; -import gdsc.binaryho.imhere.core.attendance.domain.AttendanceHistory; +import gdsc.binaryho.imhere.core.attendance.domain.Attendance; import gdsc.binaryho.imhere.core.attendance.exception.AttendanceNumberIncorrectException; import gdsc.binaryho.imhere.core.attendance.exception.AttendanceTimeExceededException; import gdsc.binaryho.imhere.core.attendance.infrastructure.AttendanceRepository; @@ -17,30 +19,33 @@ import gdsc.binaryho.imhere.core.lecture.LectureState; import gdsc.binaryho.imhere.core.lecture.application.OpenLectureService; import gdsc.binaryho.imhere.core.lecture.domain.Lecture; +import gdsc.binaryho.imhere.core.lecture.exception.LectureNotFoundException; import gdsc.binaryho.imhere.core.lecture.exception.LectureNotOpenException; +import gdsc.binaryho.imhere.core.lecture.infrastructure.LectureRepository; import gdsc.binaryho.imhere.core.member.Member; import gdsc.binaryho.imhere.security.util.AuthenticationHelper; import gdsc.binaryho.imhere.util.SeoulDateTimeHolder; import java.time.Duration; import java.time.LocalDateTime; import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; +import java.util.concurrent.CompletableFuture; import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -@Log4j2 @Service @RequiredArgsConstructor public class StudentAttendanceService { private final OpenLectureService openLectureService; + private final AttendanceSaveService attendanceSaveService; + + private final LectureRepository lectureRepository; private final AttendanceRepository attendanceRepository; private final EnrollmentInfoRepository enrollmentRepository; private final AttendanceHistoryCacheRepository attendanceHistoryCacheRepository; + private final ApplicationEventPublisher eventPublisher; private final SeoulDateTimeHolder seoulDateTimeHolder; @@ -51,31 +56,39 @@ public class StudentAttendanceService { @Transactional public void takeAttendance(AttendanceRequest attendanceRequest, Long lectureId) { Member currentStudent = authenticationHelper.getCurrentMember(); - EnrollmentInfo enrollmentInfo = enrollmentRepository - .findByMemberIdAndLectureIdAndEnrollmentState(currentStudent.getId(), lectureId, - EnrollmentState.APPROVAL) - .orElseThrow(() -> EnrollmentNotApprovedException.EXCEPTION); - validateLectureOpen(enrollmentInfo); - validateAttendanceNumber(enrollmentInfo, attendanceRequest.getAttendanceNumber()); + if (isOpenLectureCacheNotExist(currentStudent.getId(), lectureId)) { + attendWithValidateEnrollment(attendanceRequest, currentStudent, lectureId); + return; + } - attend(attendanceRequest, enrollmentInfo); + Lecture lecture = lectureRepository.findById(lectureId) + .orElseThrow(() -> LectureNotFoundException.EXCEPTION); + attend(attendanceRequest, currentStudent, lecture); } @Transactional(readOnly = true) - public StudentRecentAttendanceResponse getStudentRecentAttendance(Long lectureId) { + public StudentRecentAttendanceResponse getStudentRecentAttendanceStatus(Long lectureId) { Long studentId = authenticationHelper.getCurrentMember().getId(); + AttendanceSaveRequestStatus attendanceSaveRequestStatus = attendanceHistoryCacheRepository + .getRequestStatusByLectureIdAndStudentId(lectureId, studentId); - List attendanceHistories = attendanceHistoryCacheRepository - .findAllByLectureIdAndStudentId(lectureId, studentId); - - if (attendanceHistories.isEmpty()) { - List timestamps = getRecentAttendanceTimestamps(lectureId, studentId); - return new StudentRecentAttendanceResponse(timestamps); + if (isRequestExist(attendanceSaveRequestStatus)) { + return new StudentRecentAttendanceResponse(attendanceSaveRequestStatus); } - List timestamps = getTimestamps(attendanceHistories); - return new StudentRecentAttendanceResponse(timestamps); + return getStudentRecentAttendanceStatus(lectureId, studentId); + } + + private boolean isRequestExist(AttendanceSaveRequestStatus attendanceSaveRequestStatus) { + return !attendanceSaveRequestStatus.equals(NO_REQUEST); + } + + private StudentRecentAttendanceResponse getStudentRecentAttendanceStatus(Long lectureId, Long studentId) { + if (isRecentAttendancesExist(lectureId, studentId)) { + return new StudentRecentAttendanceResponse(SUCCESS); + } + return new StudentRecentAttendanceResponse(NO_REQUEST); } @Transactional(readOnly = true) @@ -89,50 +102,50 @@ public StudentAttendanceResponse getStudentDayAttendance(Long lectureId, Long mi return new StudentAttendanceResponse(attendances); } - private List getRecentAttendanceTimestamps(Long lectureId, Long studentId) { - List attendances = findRecentAttendances(lectureId, studentId); - List timestamps = attendances.stream() - .map(Attendance::getTimestamp) - .map(LocalDateTime::toString) - .collect(Collectors.toList()); - return timestamps; + private boolean isOpenLectureCacheNotExist(Long studentId, Long lectureId) { + return !openLectureService.isStudentOpenLectureExist(studentId, lectureId); } - private List findRecentAttendances(Long lectureId, Long studentId) { + private Boolean isRecentAttendancesExist(Long lectureId, Long studentId) { LocalDateTime now = seoulDateTimeHolder.getSeoulDateTime(); LocalDateTime beforeRecentTime = now.minusHours(RECENT_TIME.toHours()); List attendances = attendanceRepository .findByLectureIdAndStudentIdAndTimestampBetween( lectureId, studentId, beforeRecentTime, now); - return attendances; - } - - private List getTimestamps(List attendanceHistories) { - return attendanceHistories.stream() - .map(AttendanceHistory::getTimestamp) - .map(Objects::toString) - .collect(Collectors.toList()); + return !attendances.isEmpty(); } - private void validateLectureOpen(EnrollmentInfo enrollmentInfo) { if (enrollmentInfo.getLecture().getLectureState() != LectureState.OPEN) { throw LectureNotOpenException.EXCEPTION; } } - private void validateAttendanceNumber(EnrollmentInfo enrollmentInfo, int attendanceNumber) { - long lectureId = enrollmentInfo.getLecture().getId(); + private void validateAttendanceNumber(Long lectureId, int attendanceNumber) { Integer actualAttendanceNumber = openLectureService.findAttendanceNumber(lectureId); validateAttendanceNumberNotTimeOut(actualAttendanceNumber); validateAttendanceNumberCorrect(actualAttendanceNumber, attendanceNumber); } - private void attend(AttendanceRequest attendanceRequest, EnrollmentInfo enrollmentInfo) { - Member student = enrollmentInfo.getMember(); - Lecture lecture = enrollmentInfo.getLecture(); + private void attendWithValidateEnrollment( + AttendanceRequest attendanceRequest, Member student, Long lectureId) { + EnrollmentInfo enrollmentInfo = findApprovalEnrollment(lectureId, student); + validateLectureOpen(enrollmentInfo); + attend(attendanceRequest, enrollmentInfo.getMember(), enrollmentInfo.getLecture()); + } + + private EnrollmentInfo findApprovalEnrollment(Long lectureId, Member student) { + return enrollmentRepository + .findByMemberIdAndLectureIdAndEnrollmentState( + student.getId(), lectureId, EnrollmentState.APPROVAL) + .orElseThrow(() -> EnrollmentNotApprovedException.EXCEPTION); + } + + private void attend(AttendanceRequest attendanceRequest, Member student, Lecture lecture) { + validateAttendanceNumber(lecture.getId(), attendanceRequest.getAttendanceNumber()); + Attendance attendance = Attendance.createAttendance( student, lecture, attendanceRequest.getDistance(), @@ -140,24 +153,40 @@ private void attend(AttendanceRequest attendanceRequest, EnrollmentInfo enrollme seoulDateTimeHolder.from(attendanceRequest.getMilliseconds()) ); - attendanceRepository.save(attendance); - publishStudentAttendedEvent(attendance, lecture, student); - logAttendanceHistory(enrollmentInfo, attendance); + saveAttendanceAsynchronously(attendance); + publishAttendanceRequestedEvent(attendance); } - private void publishStudentAttendedEvent( - Attendance attendance, Lecture lecture, Member student) { - LocalDateTime timestamp = attendance.getTimestamp(); - eventPublisher.publishEvent( - new StudentAttendedEvent(lecture.getId(), student.getId(), timestamp)); + private void saveAttendanceAsynchronously(Attendance attendance) { + CompletableFuture.runAsync( + () -> attendanceSaveService.save(attendance) + ).thenRun( + () -> publishAttendanceSaveSucceedEvent(attendance) + ).exceptionally( + exception -> publishAttendanceFailedEvent(attendance, exception) + ); + } + + private void publishAttendanceSaveSucceedEvent(Attendance attendance) { + Long lectureId = attendance.getLecture().getId(); + Long studentId = attendance.getStudent().getId(); + AttendanceSaveSucceedEvent event = new AttendanceSaveSucceedEvent(lectureId, studentId); + eventPublisher.publishEvent(event); } - private void logAttendanceHistory(EnrollmentInfo enrollmentInfo, Attendance attendance) { - Lecture lecture = attendance.getLecture(); - Member attendMember = enrollmentInfo.getMember(); - log.info("[출석 완료] {}({}) , 학생 : {} ({})", - lecture::getLectureName, lecture::getId, - attendMember::getUnivId, attendMember::getName); + private Void publishAttendanceFailedEvent(Attendance attendance, Throwable throwable) { + Long lectureId = attendance.getLecture().getId(); + Long studentId = attendance.getStudent().getId(); + AttendanceFailedEvent event = new AttendanceFailedEvent(lectureId, studentId, throwable); + eventPublisher.publishEvent(event); + return null; + } + + private void publishAttendanceRequestedEvent(Attendance attendance) { + Long lectureId = attendance.getLecture().getId(); + Long studentId = attendance.getStudent().getId(); + eventPublisher.publishEvent( + new AttendanceRequestedEvent(lectureId, studentId)); } private void validateAttendanceNumberNotTimeOut(Integer attendanceNumber) { diff --git a/src/main/java/gdsc/binaryho/imhere/core/attendance/application/StudentAttendedEvent.java b/src/main/java/gdsc/binaryho/imhere/core/attendance/application/StudentAttendedEvent.java deleted file mode 100644 index 608b8ea..0000000 --- a/src/main/java/gdsc/binaryho/imhere/core/attendance/application/StudentAttendedEvent.java +++ /dev/null @@ -1,14 +0,0 @@ -package gdsc.binaryho.imhere.core.attendance.application; - -import java.time.LocalDateTime; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public class StudentAttendedEvent { - - private final long lectureId; - private final long studentId; - private final LocalDateTime timestamp; -} diff --git a/src/main/java/gdsc/binaryho/imhere/core/attendance/application/port/AttendanceHistoryCacheRepository.java b/src/main/java/gdsc/binaryho/imhere/core/attendance/application/port/AttendanceHistoryCacheRepository.java index 56d56d7..b35c134 100644 --- a/src/main/java/gdsc/binaryho/imhere/core/attendance/application/port/AttendanceHistoryCacheRepository.java +++ b/src/main/java/gdsc/binaryho/imhere/core/attendance/application/port/AttendanceHistoryCacheRepository.java @@ -1,11 +1,11 @@ package gdsc.binaryho.imhere.core.attendance.application.port; +import gdsc.binaryho.imhere.core.attendance.application.AttendanceSaveRequestStatus; import gdsc.binaryho.imhere.core.attendance.domain.AttendanceHistory; -import java.util.List; public interface AttendanceHistoryCacheRepository { - List findAllByLectureIdAndStudentId(long lectureId, long studentId); - void cache(AttendanceHistory attendanceHistory); + + AttendanceSaveRequestStatus getRequestStatusByLectureIdAndStudentId(Long lectureId, Long studentId); } diff --git a/src/main/java/gdsc/binaryho/imhere/core/attendance/controller/AttendanceController.java b/src/main/java/gdsc/binaryho/imhere/core/attendance/controller/AttendanceController.java index fd009fb..2288bc0 100644 --- a/src/main/java/gdsc/binaryho/imhere/core/attendance/controller/AttendanceController.java +++ b/src/main/java/gdsc/binaryho/imhere/core/attendance/controller/AttendanceController.java @@ -58,7 +58,7 @@ public ResponseEntity getStudentDayAttendance( public ResponseEntity getStudentRecentAttendance( @PathVariable("lecture_id") Long lectureId) { return ResponseEntity.ok( - studentAttendanceService.getStudentRecentAttendance(lectureId)); + studentAttendanceService.getStudentRecentAttendanceStatus(lectureId)); } @Operation(summary = "강사가 지정 강의의 출석 정보 전체를 가져오는 API") diff --git a/src/main/java/gdsc/binaryho/imhere/core/attendance/domain/AttendanceHistory.java b/src/main/java/gdsc/binaryho/imhere/core/attendance/domain/AttendanceHistory.java index c634350..93c4711 100644 --- a/src/main/java/gdsc/binaryho/imhere/core/attendance/domain/AttendanceHistory.java +++ b/src/main/java/gdsc/binaryho/imhere/core/attendance/domain/AttendanceHistory.java @@ -1,6 +1,11 @@ package gdsc.binaryho.imhere.core.attendance.domain; +import static gdsc.binaryho.imhere.core.attendance.application.AttendanceSaveRequestStatus.SUCCESS; +import static gdsc.binaryho.imhere.core.attendance.application.AttendanceSaveRequestStatus.PROCESSING; +import static gdsc.binaryho.imhere.core.attendance.application.AttendanceSaveRequestStatus.FAILED; + import gdsc.binaryho.imhere.config.redis.RedisKeyConstants; +import gdsc.binaryho.imhere.core.attendance.application.AttendanceSaveRequestStatus; import gdsc.binaryho.imhere.domain.CacheEntity; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -13,10 +18,18 @@ public class AttendanceHistory extends CacheEntity { private final long lectureId; private final long studentId; - private final String timestamp; + private final AttendanceSaveRequestStatus attendanceSaveRequestStatus; + + public static AttendanceHistory createAwaitAttendanceHistory(long lectureId, long studentId) { + return new AttendanceHistory(lectureId, studentId, PROCESSING); + } + + public static AttendanceHistory createAcceptedAttendanceHistory(long lectureId, long studentId) { + return new AttendanceHistory(lectureId, studentId, SUCCESS); + } - public static AttendanceHistory of(long lectureId, long studentId, String timestamp) { - return new AttendanceHistory(lectureId, studentId, timestamp); + public static AttendanceHistory createFailedAttendanceHistory(long lectureId, long studentId) { + return new AttendanceHistory(lectureId, studentId, FAILED); } @Override diff --git a/src/main/java/gdsc/binaryho/imhere/core/attendance/infrastructure/AttendanceHistoryCacheStrategy.java b/src/main/java/gdsc/binaryho/imhere/core/attendance/infrastructure/AttendanceHistoryCacheStrategy.java new file mode 100644 index 0000000..9ee46cc --- /dev/null +++ b/src/main/java/gdsc/binaryho/imhere/core/attendance/infrastructure/AttendanceHistoryCacheStrategy.java @@ -0,0 +1,10 @@ +package gdsc.binaryho.imhere.core.attendance.infrastructure; + +import gdsc.binaryho.imhere.core.attendance.application.AttendanceSaveRequestStatus; + +public class AttendanceHistoryCacheStrategy { + + public boolean canCache(AttendanceSaveRequestStatus newValue) { + + } +} diff --git a/src/main/java/gdsc/binaryho/imhere/core/attendance/infrastructure/AttendanceRedisCacheRepository.java b/src/main/java/gdsc/binaryho/imhere/core/attendance/infrastructure/AttendanceRedisCacheRepository.java index 77a37cd..c66ac5f 100644 --- a/src/main/java/gdsc/binaryho/imhere/core/attendance/infrastructure/AttendanceRedisCacheRepository.java +++ b/src/main/java/gdsc/binaryho/imhere/core/attendance/infrastructure/AttendanceRedisCacheRepository.java @@ -1,8 +1,8 @@ package gdsc.binaryho.imhere.core.attendance.infrastructure; +import gdsc.binaryho.imhere.core.attendance.application.AttendanceSaveRequestStatus; import gdsc.binaryho.imhere.core.attendance.application.port.AttendanceHistoryCacheRepository; import gdsc.binaryho.imhere.core.attendance.domain.AttendanceHistory; -import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @@ -19,22 +19,31 @@ public class AttendanceRedisCacheRepository implements AttendanceHistoryCacheRep private final RedisTemplate redisTemplate; @Override - public List findAllByLectureIdAndStudentId( + public AttendanceHistories findAllByLectureIdAndStudentId( final long lectureId, final long studentId) { String key = AttendanceHistory.convertToKey(lectureId, studentId); - return redisTemplate.opsForSet() + return AttendanceHistories.of( + redisTemplate.opsForSet() .members(key) .stream() .map(timestamp -> AttendanceHistory.of(lectureId, studentId, timestamp)) - .collect(Collectors.toList()); + .collect(Collectors.toList())); } @Override public void cache(AttendanceHistory attendanceHistory) { + attendanceHistory.getAttendanceSaveRequestStatus(); String key = attendanceHistory.getKey(); + String savedStatus = redisTemplate.opsForValue().get(key); redisTemplate.opsForSet() .add(key, attendanceHistory.getTimestamp()); redisTemplate.expire(key, ATTENDANCE_HISTORY_EXPIRE_HOUR, TimeUnit.HOURS); } + + @Override + public AttendanceSaveRequestStatus getRequestStatusByLectureIdAndStudentId(Long lectureId, + Long studentId) { + return null; + } } diff --git a/src/main/java/gdsc/binaryho/imhere/core/attendance/model/response/StudentRecentAttendanceResponse.java b/src/main/java/gdsc/binaryho/imhere/core/attendance/model/response/StudentRecentAttendanceResponse.java index d8c5182..5016761 100644 --- a/src/main/java/gdsc/binaryho/imhere/core/attendance/model/response/StudentRecentAttendanceResponse.java +++ b/src/main/java/gdsc/binaryho/imhere/core/attendance/model/response/StudentRecentAttendanceResponse.java @@ -1,14 +1,16 @@ package gdsc.binaryho.imhere.core.attendance.model.response; +import gdsc.binaryho.imhere.core.attendance.application.AttendanceSaveRequestStatus; import io.swagger.v3.oas.annotations.tags.Tag; -import java.util.List; import lombok.Getter; -import lombok.RequiredArgsConstructor; @Getter -@RequiredArgsConstructor -@Tag(name = "StudentRecentAttendanceResponse", description = "학생의 최근 출석 시간들") +@Tag(name = "StudentRecentAttendanceResponse", description = "학생의 최근 출석 상태") public class StudentRecentAttendanceResponse { - private final List timestamps; + private final String recentAttendanceStatus; + + public StudentRecentAttendanceResponse(AttendanceSaveRequestStatus status) { + this.recentAttendanceStatus = status.name(); + } } diff --git a/src/main/java/gdsc/binaryho/imhere/core/auth/application/EmailVerificationService.java b/src/main/java/gdsc/binaryho/imhere/core/auth/application/EmailVerificationService.java index f33a8f7..79ddb69 100644 --- a/src/main/java/gdsc/binaryho/imhere/core/auth/application/EmailVerificationService.java +++ b/src/main/java/gdsc/binaryho/imhere/core/auth/application/EmailVerificationService.java @@ -21,6 +21,7 @@ public class EmailVerificationService { private final VerificationCodeRepository verificationCodeRepository; + @Transactional public void sendVerificationCodeByEmail(String recipient) { emailFormValidator.validateEmailForm(recipient); diff --git a/src/main/java/gdsc/binaryho/imhere/core/lecture/application/AttendeeCacheService.java b/src/main/java/gdsc/binaryho/imhere/core/lecture/application/AttendeeCacheService.java deleted file mode 100644 index d30e72a..0000000 --- a/src/main/java/gdsc/binaryho/imhere/core/lecture/application/AttendeeCacheService.java +++ /dev/null @@ -1,25 +0,0 @@ -package gdsc.binaryho.imhere.core.lecture.application; - -import gdsc.binaryho.imhere.core.lecture.application.port.AttendeeCacheRepository; -import gdsc.binaryho.imhere.core.lecture.domain.AttendeeCacheEvent; -import lombok.RequiredArgsConstructor; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.event.TransactionPhase; -import org.springframework.transaction.event.TransactionalEventListener; - -@Service -@RequiredArgsConstructor -public class AttendeeCacheService { - - private final AttendeeCacheRepository attendeeCacheRepository; - - @Async - @Transactional(propagation = Propagation.REQUIRES_NEW) - @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - public void cache(AttendeeCacheEvent event) { - attendeeCacheRepository.cache(event.getLectureId(), event.getStudentIds()); - } -} diff --git a/src/main/java/gdsc/binaryho/imhere/core/lecture/application/LectureService.java b/src/main/java/gdsc/binaryho/imhere/core/lecture/application/LectureService.java index 0218c88..b5835f4 100644 --- a/src/main/java/gdsc/binaryho/imhere/core/lecture/application/LectureService.java +++ b/src/main/java/gdsc/binaryho/imhere/core/lecture/application/LectureService.java @@ -4,7 +4,6 @@ import gdsc.binaryho.imhere.core.enrollment.EnrollmentState; import gdsc.binaryho.imhere.core.enrollment.infrastructure.EnrollmentInfoRepository; import gdsc.binaryho.imhere.core.lecture.LectureState; -import gdsc.binaryho.imhere.core.lecture.application.port.AttendeeCacheRepository; import gdsc.binaryho.imhere.core.lecture.application.port.OpenLectureCacheRepository; import gdsc.binaryho.imhere.core.lecture.domain.AttendeeCacheEvent; import gdsc.binaryho.imhere.core.lecture.domain.Lecture; @@ -41,8 +40,8 @@ public class LectureService { private final AuthenticationHelper authenticationHelper; private final LectureRepository lectureRepository; private final EnrollmentInfoRepository enrollmentInfoRepository; + private final OpenLectureService openLectureService; private final OpenLectureCacheRepository openLectureCacheRepository; - private final AttendeeCacheRepository attendeeCacheRepository; private final ApplicationEventPublisher eventPublisher; @@ -95,7 +94,7 @@ public LectureResponse getStudentOpenLectures() { } private OpenLectures findCachedOpenLectures(Long studentId) { - Set lectureIds = attendeeCacheRepository.findAllAttendLectureId(studentId); + Set lectureIds = openLectureService.findAllOpenLectureIdByStudentId(studentId); List openLectures = lectureIds.stream() .map(openLectureCacheRepository::find) @@ -143,6 +142,25 @@ public AttendanceNumberResponse openLectureAndGenerateAttendanceNumber(Long lect return new AttendanceNumberResponse(attendanceNumber); } + // TODO : 테스트용 + @Transactional + public AttendanceNumberResponse openLectureAndGenerateAttendanceNumber(Long lectureId, int attendanceNumber) { + Lecture lecture = lectureRepository.findById(lectureId) + .orElseThrow(() -> LectureNotFoundException.EXCEPTION); +// authenticationHelper.verifyRequestMemberLogInMember(lecture.getMember().getId()); + + lecture.setLectureState(LectureState.OPEN); + lecture.setLastOpeningTime(seoulDateTimeHolder.getSeoulDateTime()); + + saveOpenLecture(lecture, attendanceNumber); + cacheAttendee(lecture); + + log.info("[강의 OPEN] {} ({}), 출석 번호 : " + attendanceNumber + , lecture::getLectureName, lecture::getId); + + return new AttendanceNumberResponse(attendanceNumber); + } + private void saveOpenLecture(Lecture lecture, int attendanceNumber) { OpenLecture openLecture = OpenLecture.of(lecture, attendanceNumber); openLectureCacheRepository.cache(openLecture); diff --git a/src/main/java/gdsc/binaryho/imhere/core/lecture/application/OpenLectureService.java b/src/main/java/gdsc/binaryho/imhere/core/lecture/application/OpenLectureService.java index 4af925c..022ec1b 100644 --- a/src/main/java/gdsc/binaryho/imhere/core/lecture/application/OpenLectureService.java +++ b/src/main/java/gdsc/binaryho/imhere/core/lecture/application/OpenLectureService.java @@ -1,22 +1,50 @@ package gdsc.binaryho.imhere.core.lecture.application; +import gdsc.binaryho.imhere.core.lecture.application.port.AttendeeCacheRepository; import gdsc.binaryho.imhere.core.lecture.application.port.OpenLectureCacheRepository; +import gdsc.binaryho.imhere.core.lecture.domain.AttendeeCacheEvent; import gdsc.binaryho.imhere.core.lecture.domain.OpenLecture; import java.util.Optional; +import java.util.Set; import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; @Service @RequiredArgsConstructor public class OpenLectureService { private final OpenLectureCacheRepository openLectureCacheRepository; + private final AttendeeCacheRepository attendeeCacheRepository; + @Transactional(readOnly = true) public Optional find(Long lectureId) { return openLectureCacheRepository.find(lectureId); } + @Transactional(readOnly = true) public Integer findAttendanceNumber(Long lectureId) { return openLectureCacheRepository.findAttendanceNumber(lectureId); } + + @Transactional(readOnly = true) + public Set findAllOpenLectureIdByStudentId(Long studentId) { + return attendeeCacheRepository.findAllAttendLectureId(studentId); + } + + @Transactional(readOnly = true) + public Boolean isStudentOpenLectureExist(Long studentId, Long lectureId) { + return attendeeCacheRepository.isStudentOpenLectureExist(studentId, lectureId); + } + + @Async + @Transactional(propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void cacheStudent(AttendeeCacheEvent event) { + attendeeCacheRepository.cache(event.getLectureId(), event.getStudentIds()); + } } diff --git a/src/main/java/gdsc/binaryho/imhere/core/lecture/application/port/AttendeeCacheRepository.java b/src/main/java/gdsc/binaryho/imhere/core/lecture/application/port/AttendeeCacheRepository.java index 26adda9..8461dc8 100644 --- a/src/main/java/gdsc/binaryho/imhere/core/lecture/application/port/AttendeeCacheRepository.java +++ b/src/main/java/gdsc/binaryho/imhere/core/lecture/application/port/AttendeeCacheRepository.java @@ -5,6 +5,8 @@ public interface AttendeeCacheRepository { + Boolean isStudentOpenLectureExist(Long studentId, Long lectureId); + Set findAllAttendLectureId(Long studentId); void cache(Long lectureId, StudentIds studentIds); diff --git a/src/main/java/gdsc/binaryho/imhere/core/lecture/infrastructure/AttendeeCacheRedisRepository.java b/src/main/java/gdsc/binaryho/imhere/core/lecture/infrastructure/AttendeeCacheRedisRepository.java index 410ed85..4a7a1c9 100644 --- a/src/main/java/gdsc/binaryho/imhere/core/lecture/infrastructure/AttendeeCacheRedisRepository.java +++ b/src/main/java/gdsc/binaryho/imhere/core/lecture/infrastructure/AttendeeCacheRedisRepository.java @@ -21,6 +21,12 @@ public class AttendeeCacheRedisRepository implements AttendeeCacheRepository { private static final String KEY_PREFIX = RedisKeyConstants.LECTURE_STUDENT_KEY_PREFIX; private final RedisTemplate redisTemplate; + @Override + public Boolean isStudentOpenLectureExist(Long studentId, Long lectureId) { + Set openLectureIds = findAllAttendLectureId(studentId); + return openLectureIds.contains(lectureId); + } + @Override public Set findAllAttendLectureId(Long studentId) { String queryKey = KEY_PREFIX + studentId; diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index f453f60..d724050 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -23,9 +23,9 @@ spring: ddl-auto: none dialect: -- org.hibernate.dialect.PostgreSQLDialect - properties: - hibernate: - format_sql: true +# properties: +# hibernate: +# format_sql: true logging.level: org.hibernate.SQL: debug diff --git a/src/test/java/gdsc/binaryho/imhere/core/lecture/application/AttendeeCacheServiceTest.java b/src/test/java/gdsc/binaryho/imhere/core/lecture/application/AttendeeCacheServiceTest.java index ff67075..67e5a0c 100644 --- a/src/test/java/gdsc/binaryho/imhere/core/lecture/application/AttendeeCacheServiceTest.java +++ b/src/test/java/gdsc/binaryho/imhere/core/lecture/application/AttendeeCacheServiceTest.java @@ -5,6 +5,7 @@ import static org.assertj.core.api.Assertions.assertThat; import gdsc.binaryho.imhere.core.lecture.application.port.AttendeeCacheRepository; +import gdsc.binaryho.imhere.core.lecture.application.port.OpenLectureCacheRepository; import gdsc.binaryho.imhere.core.lecture.domain.AttendeeCacheEvent; import gdsc.binaryho.imhere.core.lecture.model.StudentIds; import gdsc.binaryho.imhere.mock.TestContainer; @@ -15,20 +16,23 @@ public class AttendeeCacheServiceTest { AttendeeCacheRepository attendeeCacheRepository; - AttendeeCacheService attendeeCacheService; + OpenLectureCacheRepository openLectureCacheRepository; + OpenLectureService openLectureService; @BeforeEach void beforeEach() { TestContainer testContainer = TestContainer.builder().build(); attendeeCacheRepository = testContainer.attendeeCacheRepository; - attendeeCacheService = new AttendeeCacheService(attendeeCacheRepository); + openLectureCacheRepository = testContainer.openLectureCacheRepository; + openLectureService = new OpenLectureService( + openLectureCacheRepository, attendeeCacheRepository); } @Test void Attendee_정보를_저장할_수_있다() { // given StudentIds studentIds = new StudentIds(MOCK_STUDENT.getId()); - attendeeCacheService.cache(new AttendeeCacheEvent(MOCK_LECTURE.getId(), studentIds)); + openLectureService.cacheStudent(new AttendeeCacheEvent(MOCK_LECTURE.getId(), studentIds)); // when Set lectureIds = attendeeCacheRepository.findAllAttendLectureId(MOCK_STUDENT.getId()); diff --git a/src/test/java/gdsc/binaryho/imhere/mock/TestContainer.java b/src/test/java/gdsc/binaryho/imhere/mock/TestContainer.java index 4b9024e..fb81d50 100644 --- a/src/test/java/gdsc/binaryho/imhere/mock/TestContainer.java +++ b/src/test/java/gdsc/binaryho/imhere/mock/TestContainer.java @@ -68,7 +68,8 @@ public TestContainer( ); /* OpenLectureService 초기화 */ - openLectureService = new OpenLectureService(openLectureCacheRepository); + openLectureService = new OpenLectureService(openLectureCacheRepository, + attendeeCacheRepository); enrollmentService = new EnrollmentService( authenticationHelper, openLectureService, lectureRepository, enrollmentInfoRepository, @@ -81,14 +82,14 @@ public TestContainer( ); studentAttendanceService = new StudentAttendanceService(openLectureService, - attendanceRepository, enrollmentInfoRepository, attendanceHistoryCacheRepository, + lectureRepository, attendanceRepository, enrollmentInfoRepository, attendanceHistoryCacheRepository, applicationEventPublisher, seoulDateTimeHolder, authenticationHelper ); /* LectureService 초기화 */ lectureService = new LectureService( authenticationHelper, lectureRepository, enrollmentInfoRepository, - openLectureCacheRepository, attendeeCacheRepository, applicationEventPublisher, + openLectureService, openLectureCacheRepository, applicationEventPublisher, seoulDateTimeHolder ); } diff --git a/src/test/java/gdsc/binaryho/imhere/mock/fakerepository/FakeAttendanceHistoryCacheRepository.java b/src/test/java/gdsc/binaryho/imhere/mock/fakerepository/FakeAttendanceHistoryCacheRepository.java index a7e8d11..28aa41d 100644 --- a/src/test/java/gdsc/binaryho/imhere/mock/fakerepository/FakeAttendanceHistoryCacheRepository.java +++ b/src/test/java/gdsc/binaryho/imhere/mock/fakerepository/FakeAttendanceHistoryCacheRepository.java @@ -1,5 +1,6 @@ package gdsc.binaryho.imhere.mock.fakerepository; +import gdsc.binaryho.imhere.core.attendance.application.AttendanceSaveRequestStatus; import gdsc.binaryho.imhere.core.attendance.application.port.AttendanceHistoryCacheRepository; import gdsc.binaryho.imhere.core.attendance.domain.AttendanceHistory; import java.util.Collections; @@ -15,16 +16,25 @@ public class FakeAttendanceHistoryCacheRepository implements AttendanceHistoryCa private final Map> data = new HashMap<>(); @Override - public List findAllByLectureIdAndStudentId(final long lectureId, final long studentId) { - return data.getOrDefault( - AttendanceHistory.convertToKey(lectureId, studentId), Collections.emptySet()) + public AttendanceHistories findAllByLectureIdAndStudentId(final long lectureId, final long studentId) { + List attendanceHistories = data.getOrDefault( + AttendanceHistory.convertToKey(lectureId, studentId), Collections.emptySet()) .stream() .map(timestamp -> new AttendanceHistory(lectureId, studentId, timestamp)) .collect(Collectors.toList()); + return AttendanceHistories.of(attendanceHistories); } @Override public void cache(AttendanceHistory attendanceHistory) { data.putIfAbsent(attendanceHistory.getKey(), new HashSet<>()); } + + @Override + public AttendanceSaveRequestStatus getRequestStatusByLectureIdAndStudentId( + Long lectureId, Long studentId) { + + data.getOrDefault(AttendanceHistory.convertToKey(lectureId, studentId)) + return null; + } } diff --git a/src/test/java/gdsc/binaryho/imhere/mock/fakerepository/FakeAttendeeCacheRepository.java b/src/test/java/gdsc/binaryho/imhere/mock/fakerepository/FakeAttendeeCacheRepository.java index a73e450..227cc56 100644 --- a/src/test/java/gdsc/binaryho/imhere/mock/fakerepository/FakeAttendeeCacheRepository.java +++ b/src/test/java/gdsc/binaryho/imhere/mock/fakerepository/FakeAttendeeCacheRepository.java @@ -12,6 +12,12 @@ public class FakeAttendeeCacheRepository implements AttendeeCacheRepository { private final Map> data = new HashMap<>(); + @Override + public Boolean isStudentOpenLectureExist(Long studentId, Long lectureId) { + Set openLectureIds = findAllAttendLectureId(studentId); + return openLectureIds.contains(lectureId); + } + @Override public Set findAllAttendLectureId(Long studentId) { return data.getOrDefault( diff --git a/src/test/java/gdsc/binaryho/imhere/presentation/AttendanceControllerTest.java b/src/test/java/gdsc/binaryho/imhere/presentation/AttendanceControllerTest.java index 53ae82a..c3f63b5 100644 --- a/src/test/java/gdsc/binaryho/imhere/presentation/AttendanceControllerTest.java +++ b/src/test/java/gdsc/binaryho/imhere/presentation/AttendanceControllerTest.java @@ -79,7 +79,7 @@ public class AttendanceControllerTest { long milliseconds = MOCK_ATTENDANCE.getTimestamp().toInstant(ZoneOffset.UTC).toEpochMilli(); StudentRecentAttendanceResponse response = new StudentRecentAttendanceResponse( List.of(MOCK_ATTENDANCE.getTimestamp().toString())); - given(studentAttendanceService.getStudentRecentAttendance(lectureId)) + given(studentAttendanceService.getStudentRecentAttendanceStatus(lectureId)) .willReturn(response); mockMvc.perform( diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index b834b61..7ac40ca 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -13,17 +13,26 @@ spring: jpa: open-in-view: false + hibernate: + ddl-auto: none + # 원본 - h2: - console: - enabled: true - path: /h2-console +# h2: +# console: +# enabled: true +# path: /h2-console +# datasource: +# url: jdbc:h2:~/mem-data; +# driverClassName: org.h2.Driver +# username: sa +# password: + datasource: - url: jdbc:h2:~/mem-data; - driverClassName: org.h2.Driver - username: sa - password: + url: jdbc:postgresql://jinho-database.ckddmrlccdc1.ap-northeast-2.rds.amazonaws.com:5432/imhere-dev + username: postgres + password: d0YhFa6o3HJ8GqOzKhF5 +# driver-class-name: org.postgresql.Drive redis: host: localhost