diff --git a/src/main/java/com/gdschongik/gdsc/domain/studyv2/api/StudentAttendanceControllerV2.java b/src/main/java/com/gdschongik/gdsc/domain/studyv2/api/StudentAttendanceControllerV2.java new file mode 100644 index 000000000..c7810696e --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/studyv2/api/StudentAttendanceControllerV2.java @@ -0,0 +1,32 @@ +package com.gdschongik.gdsc.domain.studyv2.api; + +import com.gdschongik.gdsc.domain.studyv2.application.StudentStudyServiceV2; +import com.gdschongik.gdsc.domain.studyv2.dto.request.AttendanceCreateRequest; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Student Study Attendance V2", description = "사용자 스터디 출석체크 V2 API입니다.") +@RestController +@RequestMapping("/v2/attendances") +@RequiredArgsConstructor +public class StudentAttendanceControllerV2 { + + private final StudentStudyServiceV2 studentStudyServiceV2; + + @Operation(summary = "스터디 출석체크", description = "스터디에 출석체크합니다.") + @PostMapping("/attend") + public ResponseEntity attend( + @RequestParam(name = "studySessionId") Long studySessionId, + @Valid @RequestBody AttendanceCreateRequest request) { + studentStudyServiceV2.attend(studySessionId, request); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/studyv2/application/StudentStudyServiceV2.java b/src/main/java/com/gdschongik/gdsc/domain/studyv2/application/StudentStudyServiceV2.java new file mode 100644 index 000000000..1830793c2 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/studyv2/application/StudentStudyServiceV2.java @@ -0,0 +1,54 @@ +package com.gdschongik.gdsc.domain.studyv2.application; + +import static com.gdschongik.gdsc.global.exception.ErrorCode.STUDY_NOT_FOUND; + +import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.studyv2.dao.AttendanceV2Repository; +import com.gdschongik.gdsc.domain.studyv2.dao.StudyHistoryV2Repository; +import com.gdschongik.gdsc.domain.studyv2.dao.StudyV2Repository; +import com.gdschongik.gdsc.domain.studyv2.domain.AttendanceV2; +import com.gdschongik.gdsc.domain.studyv2.domain.AttendanceValidatorV2; +import com.gdschongik.gdsc.domain.studyv2.domain.StudySessionV2; +import com.gdschongik.gdsc.domain.studyv2.domain.StudyV2; +import com.gdschongik.gdsc.domain.studyv2.dto.request.AttendanceCreateRequest; +import com.gdschongik.gdsc.global.exception.CustomException; +import com.gdschongik.gdsc.global.util.MemberUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class StudentStudyServiceV2 { + + private final MemberUtil memberUtil; + private final StudyV2Repository studyV2Repository; + private final AttendanceV2Repository attendanceV2Repository; + private final StudyHistoryV2Repository studyHistoryV2Repository; + private final AttendanceValidatorV2 attendanceValidatorV2; + + @Transactional + public void attend(Long studySessionId, AttendanceCreateRequest request) { + Member currentMember = memberUtil.getCurrentMember(); + StudyV2 study = studyV2Repository + .findFetchBySessionId(studySessionId) + .orElseThrow(() -> new CustomException(STUDY_NOT_FOUND)); + StudySessionV2 studySession = study.getStudySession(studySessionId); + + boolean isAppliedToStudy = studyHistoryV2Repository.existsByStudentAndStudy(currentMember, study); + boolean isAlreadyAttended = attendanceV2Repository.existsByStudentAndStudySession(currentMember, studySession); + + attendanceValidatorV2.validateAttendance( + studySession, request.attendanceNumber(), isAppliedToStudy, isAlreadyAttended); + + AttendanceV2 attendance = AttendanceV2.create(currentMember, studySession); + attendanceV2Repository.save(attendance); + + log.info( + "[StudentStudyServiceV2] 스터디 출석체크: attendanceId={}, memberId={}", + attendance.getId(), + currentMember.getId()); + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/studyv2/dao/AttendanceV2Repository.java b/src/main/java/com/gdschongik/gdsc/domain/studyv2/dao/AttendanceV2Repository.java new file mode 100644 index 000000000..0bae57180 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/studyv2/dao/AttendanceV2Repository.java @@ -0,0 +1,11 @@ +package com.gdschongik.gdsc.domain.studyv2.dao; + +import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.studyv2.domain.AttendanceV2; +import com.gdschongik.gdsc.domain.studyv2.domain.StudySessionV2; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AttendanceV2Repository extends JpaRepository { + + boolean existsByStudentAndStudySession(Member student, StudySessionV2 studySession); +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/studyv2/dao/StudyHistoryV2Repository.java b/src/main/java/com/gdschongik/gdsc/domain/studyv2/dao/StudyHistoryV2Repository.java index 0a3b7eb38..d1dd24d61 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/studyv2/dao/StudyHistoryV2Repository.java +++ b/src/main/java/com/gdschongik/gdsc/domain/studyv2/dao/StudyHistoryV2Repository.java @@ -8,4 +8,6 @@ public interface StudyHistoryV2Repository extends JpaRepository, StudyHistoryV2CustomRepository { Optional findByStudentAndStudy(Member student, StudyV2 study); + + boolean existsByStudentAndStudy(Member currentMember, StudyV2 study); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/studyv2/domain/AttendanceValidatorV2.java b/src/main/java/com/gdschongik/gdsc/domain/studyv2/domain/AttendanceValidatorV2.java new file mode 100644 index 000000000..11e6098e8 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/studyv2/domain/AttendanceValidatorV2.java @@ -0,0 +1,36 @@ +package com.gdschongik.gdsc.domain.studyv2.domain; + +import static com.gdschongik.gdsc.global.exception.ErrorCode.ATTENDANCE_NUMBER_MISMATCH; +import static com.gdschongik.gdsc.global.exception.ErrorCode.ATTENDANCE_PERIOD_INVALID; +import static com.gdschongik.gdsc.global.exception.ErrorCode.STUDY_HISTORY_NOT_FOUND; +import static com.gdschongik.gdsc.global.exception.ErrorCode.STUDY_SESSION_ALREADY_ATTENDED; + +import com.gdschongik.gdsc.global.annotation.DomainService; +import com.gdschongik.gdsc.global.exception.CustomException; + +@DomainService +public class AttendanceValidatorV2 { + + public void validateAttendance( + StudySessionV2 studySession, String attendanceNumber, boolean isAppliedToStudy, boolean isAlreadyAttended) { + // 스터디 신청 여부 검증 + if (!isAppliedToStudy) { + throw new CustomException(STUDY_HISTORY_NOT_FOUND); + } + + // 스터디 중복 출석체크 여부 검증 + if (isAlreadyAttended) { + throw new CustomException(STUDY_SESSION_ALREADY_ATTENDED); + } + + // 출석체크 가능 기간 검증 + if (!studySession.getLessonPeriod().isOpen()) { + throw new CustomException(ATTENDANCE_PERIOD_INVALID); + } + + // 출석체크 번호 검증 + if (!studySession.getLessonAttendanceNumber().equals(attendanceNumber)) { + throw new CustomException(ATTENDANCE_NUMBER_MISMATCH); + } + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/studyv2/dto/request/AttendanceCreateRequest.java b/src/main/java/com/gdschongik/gdsc/domain/studyv2/dto/request/AttendanceCreateRequest.java new file mode 100644 index 000000000..9ffb7a90e --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/studyv2/dto/request/AttendanceCreateRequest.java @@ -0,0 +1,13 @@ +package com.gdschongik.gdsc.domain.studyv2.dto.request; + +import static com.gdschongik.gdsc.global.common.constant.RegexConstant.ATTENDANCE_NUMBER; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; + +public record AttendanceCreateRequest( + @NotBlank + @Pattern(regexp = ATTENDANCE_NUMBER, message = "출석번호는 " + ATTENDANCE_NUMBER + " 형식이어야 합니다.") + @Schema(description = "출석번호") + String attendanceNumber) {} diff --git a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java index f7ff29601..a0ac52337 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -138,8 +138,10 @@ public enum ErrorCode { // Attendance ATTENDANCE_DATE_INVALID(HttpStatus.CONFLICT, "강의일이 아니면 출석체크할 수 없습니다."), + ATTENDANCE_PERIOD_INVALID(HttpStatus.CONFLICT, "강의시간이 아니면 출석체크할 수 없습니다."), ATTENDANCE_NUMBER_MISMATCH(HttpStatus.CONFLICT, "출석번호가 일치하지 않습니다."), STUDY_DETAIL_ALREADY_ATTENDED(HttpStatus.CONFLICT, "이미 출석 처리된 스터디입니다."), + STUDY_SESSION_ALREADY_ATTENDED(HttpStatus.CONFLICT, "이미 출석 처리된 스터디 회차입니다."), // Order ORDER_NOT_FOUND(HttpStatus.NOT_FOUND, "주문이 존재하지 않습니다."),