Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

πŸš€ [feature] 학생 μΆœμ„ 확인 API κ΅¬ν˜„ #121

Merged
merged 27 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a59b4a8
refactor : AttendanceService의 public method듀을 μ΅œμƒλ‹¨μœΌλ‘œ 이동 (#117)
binary-ho Mar 13, 2024
a36e677
refactor : Attendance의 Member 이름을 student둜 λ³€κ²½ (#117)
binary-ho Mar 13, 2024
3995871
refactor : AttendanceResponseλ₯Ό μš©λ„μ— 맞게 AttendancesResponse둜 이름 λ³€κ²½ (#117)
binary-ho Mar 13, 2024
b3d4576
feat : ν•™μƒμ˜ μΆœμ„ 기둝 확인 κΈ°λŠ₯ κ΅¬ν˜„ (#117)
binary-ho Mar 13, 2024
350d251
refactor : κ°•μ‚¬μ˜ μΆœμ„ 정보 쑰회 응닡에 κ°•μ˜ 정보 제거 (#117)
binary-ho Mar 13, 2024
30e767c
refactor : μΆœμ„μ‹œ λ‘œκΉ… λ©”μ„œλ“œ 뢄리 (#117)
binary-ho Mar 13, 2024
2fbd6f2
refactor : μœ μ € κΆŒν•œ 확인 λ©”μ„œλ“œκ°€ 검증할 κΆŒν•œμ„ μž…λ ₯ 받도둝 λ³€κ²½ (#117)
binary-ho Mar 13, 2024
ffb3e4c
refactor : κ°•μ‚¬μš© μΆœμ„ 기둝 확인 API에 강사 κΆŒν•œ 검증 μΆ”κ°€ (#117)
binary-ho Mar 13, 2024
e91ea52
refactor : ν•™μƒμ˜ 졜근 μΆœμ„ 정보λ₯Ό κ°€μ Έμ˜€λŠ” κΈ°λŠ₯ κ΅¬ν˜„ (#117)
binary-ho Mar 13, 2024
d3de37c
chore : OpenLecture의 정적 νŒ©ν„°λ¦¬ λ©”μ„œλ“œ 이름 λ³€κ²½ (#117)
binary-ho Mar 13, 2024
ecb4eb4
refactor : keyλ₯Ό 슀슀둜 λ§Œλ“œλŠ” 것을 κ°•μ œν•˜λŠ” 좔상 클래슀 CacheEntity κ΅¬ν˜„ (#117)
binary-ho Mar 13, 2024
53f0953
feat : μΆœμ„ 기둝을 μ €μž₯ν•˜λŠ” AttendanceHistory κ΅¬ν˜„ (#117)
binary-ho Mar 13, 2024
e025891
feat :AttendanceHistoryλ₯Ό μΊμ‹±ν•˜λŠ” AttendanceHistoryCacheRepository κ΅¬ν˜„ (#117)
binary-ho Mar 13, 2024
5f8da90
feat :StudentAttendedEvent와 AttendanceHistoryλ₯Ό μΊμ‹±ν•˜λŠ” Listener κ΅¬ν˜„ (#117)
binary-ho Mar 13, 2024
2e95e89
refactor : RedisKeyPrefixes의 이름을 RedisKeyConstants둜 λ³€κ²½ (#117)
binary-ho Mar 13, 2024
bdf75eb
feat : 학생 μΆœμ„μ‹œ StudentAttendedEventλ₯Ό λ°œν–‰ν•˜λŠ” κΈ°λŠ₯ κ΅¬ν˜„ (#117)
binary-ho Mar 13, 2024
3ebaf4d
feat : Look Aside둜 졜근 μΆœμ„ 내역을 κ°€μ Έμ˜€λŠ” getStudentRecentAttendance κ΅¬ν˜„ (#117)
binary-ho Mar 13, 2024
8ea6eac
refactor : AttendanceServiceλ₯Ό 학생, 강사λ₯Ό κΈ°μ€€μœΌλ‘œ 뢄리 (#117)
binary-ho Mar 13, 2024
5a3e0d7
feat : ν…ŒμŠ€νŠΈλ₯Ό μœ„ν•œ FakeAttendanceHistoryCacheRepository κ΅¬ν˜„ (#117)
binary-ho Mar 14, 2024
f4b6340
refactor : κ°•μ‚¬μš© μΆœμ„ 정보 APIμ—μ„œ μš”μ²­μžκ°€ 강사인지 κ²€μ¦ν•˜λŠ” λ©”μ„œλ“œ μΆ”κ°€ (#117)
binary-ho Mar 14, 2024
355cd30
test : μ½”λ“œ 변경에 맞좰 ν…ŒμŠ€νŠΈ μ½”λ“œ λ³€κ²½ (#117)
binary-ho Mar 14, 2024
9716184
chore : Attendance 객체 νŒ¨ν‚€μ§€ λ³€κ²½ (#117)
binary-ho Mar 14, 2024
5e6defc
refactor : AttendanceController Endpoint Restfulν•˜κ²Œ λ³€κ²½ (#117)
binary-ho Mar 14, 2024
1430ad9
refactor : 학생 μΆœμ„ 쑰회 API κ΅¬ν˜„ (#117)
binary-ho Mar 14, 2024
d0d5a5e
test : 학생 μΆœμ„ 쑰회 API Controller Slice Test κ΅¬ν˜„ (#117)
binary-ho Mar 14, 2024
e347370
chore : Response 객체 νŒ¨ν‚€μ§€ λ³€κ²½ (#117)
binary-ho Mar 14, 2024
e23b182
refactor : AttendanceHistory 객체 캐싱 μ‹œκ°„ μ„€μ • (#117)
binary-ho Mar 14, 2024
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
@@ -1,9 +1,10 @@
package gdsc.binaryho.imhere.config.redis;

public class RedisKeyPrefixes {
public class RedisKeyConstants {

public static final String ATTENDANCE_NUMBER_KEY_PREFIX = "$lecture_id$";
public static final String VERIFICATION_CODE_KEY_PREFIX = "$email$";
public static final String OPEN_LECTURE_KEY_PREFIX = "$open_lecture$";
public static final String LECTURE_STUDENT_KEY_PREFIX = "$lecture_student$";
public static final String ATTENDANCE_HISTORY_KEY_FORMAT = "$attendance_history$lecture_id:%d$student_id:%d$";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package gdsc.binaryho.imhere.core.attendance.application;

import gdsc.binaryho.imhere.core.attendance.application.port.AttendanceHistoryCacheRepository;
import gdsc.binaryho.imhere.core.attendance.domain.AttendanceHistory;
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 AttendanceHistoryCacheService {

private final AttendanceHistoryCacheRepository attendanceHistoryCacheRepository;

@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void cache(StudentAttendedEvent event) {
long lectureId = event.getLectureId();
long studentId = event.getStudentId();
String timestamp = event.getTimestamp().toString();

AttendanceHistory attendanceHistory = AttendanceHistory.of(
lectureId, studentId, timestamp);
attendanceHistoryCacheRepository.cache(attendanceHistory);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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.attendance.model.response.LecturerAttendanceResponse;
import gdsc.binaryho.imhere.core.lecture.domain.Lecture;
import gdsc.binaryho.imhere.core.lecture.exception.LectureNotFoundException;
import gdsc.binaryho.imhere.core.lecture.infrastructure.LectureRepository;
import gdsc.binaryho.imhere.core.member.Role;
import gdsc.binaryho.imhere.security.util.AuthenticationHelper;
import gdsc.binaryho.imhere.util.SeoulDateTimeHolder;
import java.time.LocalDateTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Log4j2
@Service
@RequiredArgsConstructor
public class LecturerAttendanceService {

private final AttendanceRepository attendanceRepository;
private final LectureRepository lectureRepository;

private final SeoulDateTimeHolder seoulDateTimeHolder;
private final AuthenticationHelper authenticationHelper;

@Transactional(readOnly = true)
public LecturerAttendanceResponse getLecturerAttendances(Long lectureId) {
authenticationHelper.verifyMemberHasRole(Role.LECTURER);

List<Attendance> attendances = attendanceRepository.findAllByLectureId(lectureId);
validateLectureOwnerRequested(lectureId, attendances);
return new LecturerAttendanceResponse(attendances);
}

@Transactional(readOnly = true)
public LecturerAttendanceResponse getLecturerDayAttendances(Long lectureId, Long milliseconds) {
authenticationHelper.verifyMemberHasRole(Role.LECTURER);

LocalDateTime timestamp = getTodaySeoulDateTime(milliseconds);
List<Attendance> attendances = attendanceRepository
.findByLectureIdAndTimestampBetween(lectureId, timestamp, timestamp.plusDays(1));
validateLectureOwnerRequested(lectureId, attendances);
return new LecturerAttendanceResponse(attendances);
}

private LocalDateTime getTodaySeoulDateTime(Long milliseconds) {
return seoulDateTimeHolder.from(milliseconds)
.withHour(0).withMinute(0).withSecond(0);
}

private void validateLectureOwnerRequested(Long lectureId, List<Attendance> attendances) {
attendances.stream()
.findAny()
.ifPresentOrElse(
attendance -> authenticationHelper.verifyRequestMemberLogInMember(getLecturerId(attendance)),
() -> authenticationHelper.verifyRequestMemberLogInMember(getLecturerId(lectureId))
);
}

private long getLecturerId(Attendance attendance) {
Lecture lecture = attendance.getLecture();
return lecture.getMember().getId();
}

private long getLecturerId(Long id) {
Lecture lecture = lectureRepository.findById(id)
.orElseThrow(() -> LectureNotFoundException.EXCEPTION);
return lecture.getMember().getId();
}
}
Original file line number Diff line number Diff line change
@@ -1,50 +1,59 @@
package gdsc.binaryho.imhere.core.attendance.application;


import gdsc.binaryho.imhere.core.attendance.Attendance;
import gdsc.binaryho.imhere.core.attendance.domain.Attendance;
import gdsc.binaryho.imhere.core.attendance.application.port.AttendanceHistoryCacheRepository;
import gdsc.binaryho.imhere.core.attendance.domain.AttendanceHistory;
import gdsc.binaryho.imhere.core.attendance.exception.AttendanceNumberIncorrectException;
import gdsc.binaryho.imhere.core.attendance.exception.AttendanceTimeExceededException;
import gdsc.binaryho.imhere.core.attendance.infrastructure.AttendanceRepository;
import gdsc.binaryho.imhere.core.attendance.model.request.AttendanceRequest;
import gdsc.binaryho.imhere.core.attendance.model.response.AttendanceResponse;
import gdsc.binaryho.imhere.core.attendance.model.response.StudentAttendanceResponse;
import gdsc.binaryho.imhere.core.attendance.model.response.StudentRecentAttendanceResponse;
import gdsc.binaryho.imhere.core.enrollment.EnrollmentInfo;
import gdsc.binaryho.imhere.core.enrollment.EnrollmentState;
import gdsc.binaryho.imhere.core.enrollment.exception.EnrollmentNotApprovedException;
import gdsc.binaryho.imhere.core.enrollment.infrastructure.EnrollmentInfoRepository;
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.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
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 AttendanceService {
public class StudentAttendanceService {

private final AuthenticationHelper authenticationHelper;
private final OpenLectureService openLectureService;
private final AttendanceRepository attendanceRepository;
private final EnrollmentInfoRepository enrollmentRepository;
private final LectureRepository lectureRepository;
private final AttendanceHistoryCacheRepository attendanceHistoryCacheRepository;
private final ApplicationEventPublisher eventPublisher;

private final SeoulDateTimeHolder seoulDateTimeHolder;
private final AuthenticationHelper authenticationHelper;

private final static Duration RECENT_TIME = Duration.ofHours(1L);

@Transactional
public void takeAttendance(AttendanceRequest attendanceRequest, Long lectureId) {
Member currentStudent = authenticationHelper.getCurrentMember();
EnrollmentInfo enrollmentInfo = enrollmentRepository
.findByMemberIdAndLectureIdAndEnrollmentState(currentStudent.getId(), lectureId, EnrollmentState.APPROVAL)
.findByMemberIdAndLectureIdAndEnrollmentState(currentStudent.getId(), lectureId,
EnrollmentState.APPROVAL)
.orElseThrow(() -> EnrollmentNotApprovedException.EXCEPTION);

validateLectureOpen(enrollmentInfo);
Expand All @@ -53,25 +62,60 @@ public void takeAttendance(AttendanceRequest attendanceRequest, Long lectureId)
attend(attendanceRequest, enrollmentInfo);
}

private void attend(AttendanceRequest attendanceRequest, EnrollmentInfo enrollmentInfo) {
@Transactional(readOnly = true)
public StudentRecentAttendanceResponse getStudentRecentAttendance(Long lectureId) {
Long studentId = authenticationHelper.getCurrentMember().getId();

Attendance attendance = Attendance.createAttendance(
enrollmentInfo.getMember(),
enrollmentInfo.getLecture(),
attendanceRequest.getDistance(),
attendanceRequest.getAccuracy(),
seoulDateTimeHolder.from(attendanceRequest.getMilliseconds())
);
List<AttendanceHistory> attendanceHistories = attendanceHistoryCacheRepository
.findAllByLectureIdAndStudentId(lectureId, studentId);

attendanceRepository.save(attendance);
if (attendanceHistories.isEmpty()) {
List<String> timestamps = getRecentAttendanceTimestamps(lectureId, studentId);
return new StudentRecentAttendanceResponse(timestamps);
}

Lecture lecture = attendance.getLecture();
Member attendMember = enrollmentInfo.getMember();
log.info("[μΆœμ„ μ™„λ£Œ] {}({}) , 학생 : {} ({})",
lecture::getLectureName, lecture::getId,
attendMember::getUnivId, attendMember::getName);
List<String> timestamps = getTimestamps(attendanceHistories);
return new StudentRecentAttendanceResponse(timestamps);
}

@Transactional(readOnly = true)
public StudentAttendanceResponse getStudentDayAttendance(Long lectureId, Long milliseconds) {
LocalDateTime timestamp = getTodaySeoulDateTime(milliseconds);
Long studentId = authenticationHelper.getCurrentMember().getId();
List<Attendance> attendances = attendanceRepository
.findByLectureIdAndStudentIdAndTimestampBetween(
lectureId, studentId, timestamp, timestamp.plusDays(1));

return new StudentAttendanceResponse(attendances);
}

private List<String> getRecentAttendanceTimestamps(Long lectureId, Long studentId) {
List<Attendance> attendances = findRecentAttendances(lectureId, studentId);
List<String> timestamps = attendances.stream()
.map(Attendance::getTimestamp)
.map(LocalDateTime::toString)
.collect(Collectors.toList());
return timestamps;
}

private List<Attendance> findRecentAttendances(Long lectureId, Long studentId) {
LocalDateTime now = seoulDateTimeHolder.getSeoulDateTime();
LocalDateTime beforeRecentTime = now.minusHours(RECENT_TIME.toHours());

List<Attendance> attendances = attendanceRepository
.findByLectureIdAndStudentIdAndTimestampBetween(
lectureId, studentId, beforeRecentTime, now);
return attendances;
}

private List<String> getTimestamps(List<AttendanceHistory> attendanceHistories) {
return attendanceHistories.stream()
.map(AttendanceHistory::getTimestamp)
.map(Objects::toString)
.collect(Collectors.toList());
}


private void validateLectureOpen(EnrollmentInfo enrollmentInfo) {
if (enrollmentInfo.getLecture().getLectureState() != LectureState.OPEN) {
throw LectureNotOpenException.EXCEPTION;
Expand All @@ -86,55 +130,47 @@ private void validateAttendanceNumber(EnrollmentInfo enrollmentInfo, int attenda
validateAttendanceNumberCorrect(actualAttendanceNumber, attendanceNumber);
}

private void validateAttendanceNumberNotTimeOut(Integer attendanceNumber) {
if (attendanceNumber == null) {
throw AttendanceTimeExceededException.EXCEPTION;
}
}
private void attend(AttendanceRequest attendanceRequest, EnrollmentInfo enrollmentInfo) {
Member student = enrollmentInfo.getMember();
Lecture lecture = enrollmentInfo.getLecture();
Attendance attendance = Attendance.createAttendance(
student, lecture,
attendanceRequest.getDistance(),
attendanceRequest.getAccuracy(),
seoulDateTimeHolder.from(attendanceRequest.getMilliseconds())
);

private void validateAttendanceNumberCorrect(Integer actualAttendanceNumber, int attendanceNumber) {
if (actualAttendanceNumber != attendanceNumber) {
throw AttendanceNumberIncorrectException.EXCEPTION;
}
attendanceRepository.save(attendance);
publishStudentAttendedEvent(attendance, lecture, student);
logAttendanceHistory(enrollmentInfo, attendance);
}

@Transactional(readOnly = true)
public AttendanceResponse getAttendances(Long lectureId) {
List<Attendance> attendances = attendanceRepository.findAllByLectureId(lectureId);

if (attendances.isEmpty()) {
return getNullAttendanceDto(lectureId);
}

Lecture lecture = attendances.get(0).getLecture();
verifyRequestMemberLogInMember(lecture.getMember());

return new AttendanceResponse(lecture, attendances);
private void publishStudentAttendedEvent(
Attendance attendance, Lecture lecture, Member student) {
LocalDateTime timestamp = attendance.getTimestamp();
eventPublisher.publishEvent(
new StudentAttendedEvent(lecture.getId(), student.getId(), timestamp));
}

private void verifyRequestMemberLogInMember(Member lecturer) {
authenticationHelper.verifyRequestMemberLogInMember(lecturer.getId());
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 AttendanceResponse getNullAttendanceDto(Long lectureId) {
Lecture lecture = lectureRepository.findById(lectureId)
.orElseThrow(() -> LectureNotFoundException.EXCEPTION);
return new AttendanceResponse(lecture, Collections.emptyList());
private void validateAttendanceNumberNotTimeOut(Integer attendanceNumber) {
if (attendanceNumber == null) {
throw AttendanceTimeExceededException.EXCEPTION;
}
}

@Transactional(readOnly = true)
public AttendanceResponse getDayAttendances(Long lectureId, Long milliseconds) {
LocalDateTime timestamp = getTodaySeoulDateTime(milliseconds);
List<Attendance> attendances = attendanceRepository
.findByLectureIdAndTimestampBetween(lectureId, timestamp, timestamp.plusDays(1));

if (attendances.isEmpty()) {
return getNullAttendanceDto(lectureId);
private void validateAttendanceNumberCorrect(Integer actualAttendanceNumber,
int attendanceNumber) {
if (actualAttendanceNumber != attendanceNumber) {
throw AttendanceNumberIncorrectException.EXCEPTION;
}

Lecture lecture = attendances.get(0).getLecture();
verifyRequestMemberLogInMember(lecture.getMember());
return new AttendanceResponse(lecture, attendances);
}

private LocalDateTime getTodaySeoulDateTime(Long milliseconds) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package gdsc.binaryho.imhere.core.attendance.application.port;

import gdsc.binaryho.imhere.core.attendance.domain.AttendanceHistory;
import java.util.List;

public interface AttendanceHistoryCacheRepository {

List<AttendanceHistory> findAllByLectureIdAndStudentId(long lectureId, long studentId);

void cache(AttendanceHistory attendanceHistory);
}
Loading
Loading