diff --git a/src/docs/asciidoc/seminar/seminar-attendance.adoc b/src/docs/asciidoc/seminar/seminar-attendance.adoc index cf45452d2..53542f156 100644 --- a/src/docs/asciidoc/seminar/seminar-attendance.adoc +++ b/src/docs/asciidoc/seminar/seminar-attendance.adoc @@ -15,6 +15,8 @@ link:../keeper.html[API 목록으로 돌아가기] == *세미나 출석* +NOTE: 활동회원만 출석이 가능합니다. + === 요청 ==== Request @@ -25,6 +27,10 @@ include::{snippets}/attendance-seminar/http-request.adoc[] include::{snippets}/attendance-seminar/request-cookies.adoc[] +==== Path Parameters + +include::{snippets}/attendance-seminar/path-parameters.adoc[] + ==== Request Fields include::{snippets}/attendance-seminar/request-fields.adoc[] @@ -51,6 +57,10 @@ include::{snippets}/change-attendance-seminar/http-request.adoc[] include::{snippets}/change-attendance-seminar/request-cookies.adoc[] +==== Path Parameters + +include::{snippets}/change-attendance-seminar/path-parameters.adoc[] + ==== Request Fields include::{snippets}/change-attendance-seminar/request-fields.adoc[] diff --git a/src/docs/asciidoc/seminar/seminar.adoc b/src/docs/asciidoc/seminar/seminar.adoc index 90fa0b9f3..d0a6f9f41 100644 --- a/src/docs/asciidoc/seminar/seminar.adoc +++ b/src/docs/asciidoc/seminar/seminar.adoc @@ -47,6 +47,10 @@ include::{snippets}/start-seminar/http-request.adoc[] include::{snippets}/start-seminar/request-cookies.adoc[] +==== Path Parameters + +include::{snippets}/start-seminar/path-parameters.adoc[] + ==== Request Fields include::{snippets}/start-seminar/request-fields.adoc[] @@ -179,6 +183,54 @@ include::{snippets}/delete-seminar/request-cookies.adoc[] include::{snippets}/delete-seminar/http-response.adoc[] +== *가장 최근에 마감된 세미나 조회* + +NOTE: 어제까지의 세미나 중, 가장 현재와 가깝고 마감된 세미나를 조회합니다. + +=== 요청 + +==== Request + +include::{snippets}/get-recently-done-seminar/http-request.adoc[] + +==== Request Cookies + +include::{snippets}/get-recently-done-seminar/request-cookies.adoc[] + +=== 응답 + +==== Response + +include::{snippets}/get-recently-done-seminar/http-response.adoc[] + +==== Response Fields + +include::{snippets}/get-recently-done-seminar/response-fields.adoc[] + +== *가장 최근의 예정된 세미나 2개 조회* + +NOTE: 오늘을 포함하여 가장 최근의 예정된 세미나 2개를 조회합니다. + +=== 요청 + +==== Request + +include::{snippets}/get-recently-upcoming-seminars/http-request.adoc[] + +==== Request Cookies + +include::{snippets}/get-recently-upcoming-seminars/request-cookies.adoc[] + +=== 응답 + +==== Response + +include::{snippets}/get-recently-upcoming-seminars/http-response.adoc[] + +==== Response Fields + +include::{snippets}/get-recently-upcoming-seminars/response-fields.adoc[] + == *세미나 출석* link:seminar-attendance.html[API 문서 보기] diff --git a/src/main/java/com/keeper/homepage/domain/member/application/convenience/MemberFindService.java b/src/main/java/com/keeper/homepage/domain/member/application/convenience/MemberFindService.java index f1d1bbaba..0f532987f 100644 --- a/src/main/java/com/keeper/homepage/domain/member/application/convenience/MemberFindService.java +++ b/src/main/java/com/keeper/homepage/domain/member/application/convenience/MemberFindService.java @@ -1,11 +1,14 @@ package com.keeper.homepage.domain.member.application.convenience; +import static com.keeper.homepage.domain.member.entity.type.MemberType.MemberTypeEnum.정회원; +import static com.keeper.homepage.domain.member.entity.type.MemberType.getMemberTypeBy; import static com.keeper.homepage.global.error.ErrorCode.MEMBER_NOT_FOUND; import com.keeper.homepage.domain.member.dao.MemberRepository; import com.keeper.homepage.domain.member.entity.Member; import com.keeper.homepage.domain.member.entity.embedded.RealName; import com.keeper.homepage.global.error.BusinessException; +import java.util.List; import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -38,4 +41,11 @@ public Stream findAllByRealName(RealName realName) { return memberRepository.findAllByProfileRealNameAndIdNot(realName, VIRTUAL_MEMBER_ID) .stream(); } + + public List findAllRegular() { + return memberRepository.findAllByMemberType(getMemberTypeBy(정회원)) + .stream() + .filter(member -> member.getId() != VIRTUAL_MEMBER_ID) + .toList(); + } } diff --git a/src/main/java/com/keeper/homepage/domain/member/dao/MemberRepository.java b/src/main/java/com/keeper/homepage/domain/member/dao/MemberRepository.java index 3562c91a0..d0fdbe1e4 100644 --- a/src/main/java/com/keeper/homepage/domain/member/dao/MemberRepository.java +++ b/src/main/java/com/keeper/homepage/domain/member/dao/MemberRepository.java @@ -5,6 +5,7 @@ import com.keeper.homepage.domain.member.entity.embedded.LoginId; import com.keeper.homepage.domain.member.entity.embedded.RealName; import com.keeper.homepage.domain.member.entity.embedded.StudentId; +import com.keeper.homepage.domain.member.entity.type.MemberType; import java.util.List; import java.util.Optional; import org.springframework.data.domain.Page; @@ -35,4 +36,6 @@ Optional findByProfileEmailAddressAndProfileLoginId(EmailAddress profile List findAllByProfileRealNameAndIdNot(RealName realName, long virtualId); void deleteAllByIdNot(Long virtualId); + + List findAllByMemberType(MemberType type); } diff --git a/src/main/java/com/keeper/homepage/domain/seminar/api/SeminarAttendanceController.java b/src/main/java/com/keeper/homepage/domain/seminar/api/SeminarAttendanceController.java index 5276328fd..9f7296a99 100644 --- a/src/main/java/com/keeper/homepage/domain/seminar/api/SeminarAttendanceController.java +++ b/src/main/java/com/keeper/homepage/domain/seminar/api/SeminarAttendanceController.java @@ -12,7 +12,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; -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.RestController; @@ -24,21 +23,21 @@ public class SeminarAttendanceController { private final SeminarAttendanceService seminarAttendanceService; - @PostMapping + @PatchMapping public ResponseEntity attendanceSeminar( @PathVariable Long seminarId, @LoginMember Member member, @RequestBody @Valid SeminarAttendanceCodeRequest request) { - SeminarAttendanceResponse response = seminarAttendanceService.save(seminarId, member, request); + SeminarAttendanceResponse response = seminarAttendanceService.attendance(seminarId, member, request); return ResponseEntity.status(HttpStatus.CREATED).body(response); } - @PatchMapping + @PatchMapping("/{memberId}") // TODO: 관리자 권한으로 접근 가능하게 설정 public ResponseEntity changeAttendanceStatus( - @PathVariable Long seminarId, - @LoginMember Member member, + @PathVariable long seminarId, + @PathVariable long memberId, @RequestBody @Valid SeminarAttendanceStatusRequest request) { - seminarAttendanceService.changeStatus(seminarId, member, request); + seminarAttendanceService.changeStatus(seminarId, memberId, request); return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/com/keeper/homepage/domain/seminar/api/SeminarController.java b/src/main/java/com/keeper/homepage/domain/seminar/api/SeminarController.java index a456356c2..77ead891c 100644 --- a/src/main/java/com/keeper/homepage/domain/seminar/api/SeminarController.java +++ b/src/main/java/com/keeper/homepage/domain/seminar/api/SeminarController.java @@ -1,13 +1,17 @@ package com.keeper.homepage.domain.seminar.api; +import com.keeper.homepage.domain.member.entity.Member; import com.keeper.homepage.domain.seminar.application.SeminarService; import com.keeper.homepage.domain.seminar.dto.request.SeminarStartRequest; import com.keeper.homepage.domain.seminar.dto.response.SeminarAttendanceCodeResponse; +import com.keeper.homepage.domain.seminar.dto.response.SeminarDetailResponse; import com.keeper.homepage.domain.seminar.dto.response.SeminarIdResponse; import com.keeper.homepage.domain.seminar.dto.response.SeminarListResponse; import com.keeper.homepage.domain.seminar.dto.response.SeminarResponse; +import com.keeper.homepage.global.config.security.annotation.LoginMember; import jakarta.validation.Valid; import java.time.LocalDate; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.HttpStatus; @@ -73,8 +77,26 @@ public ResponseEntity getSeminarByDate( } @GetMapping("/{seminarId}") - public ResponseEntity getSeminar(@PathVariable long seminarId) { - SeminarResponse response = seminarService.findById(seminarId); + @Secured({"ROLE_회원"}) + public ResponseEntity getSeminar( + @LoginMember Member member, + @PathVariable long seminarId + ) { + SeminarDetailResponse response = seminarService.findById(member, seminarId); return ResponseEntity.status(HttpStatus.OK).body(response); } + + @GetMapping("/recently-done") + @Secured({"ROLE_회원"}) + public ResponseEntity getRecentlyDoneSeminar() { + SeminarIdResponse response = seminarService.getRecentlyDoneSeminar(); + return ResponseEntity.ok(response); + } + + @GetMapping("/recently-upcoming") + @Secured({"ROLE_회원"}) + public ResponseEntity> getRecentlyUpcomingSeminars() { + List responses = seminarService.getRecentlyUpcomingSeminars(); + return ResponseEntity.ok(responses); + } } diff --git a/src/main/java/com/keeper/homepage/domain/seminar/application/SeminarAttendanceService.java b/src/main/java/com/keeper/homepage/domain/seminar/application/SeminarAttendanceService.java index e2415b5e5..310864dd1 100644 --- a/src/main/java/com/keeper/homepage/domain/seminar/application/SeminarAttendanceService.java +++ b/src/main/java/com/keeper/homepage/domain/seminar/application/SeminarAttendanceService.java @@ -1,20 +1,24 @@ package com.keeper.homepage.domain.seminar.application; +import static com.keeper.homepage.domain.seminar.entity.SeminarAttendanceStatus.SeminarAttendanceStatusType.BEFORE_ATTENDANCE; import static com.keeper.homepage.global.error.ErrorCode.MEMBER_NOT_FOUND; +import static com.keeper.homepage.global.error.ErrorCode.SEMINAR_ATTENDANCE_ATTEMPT_NOT_AVAILABLE; import static com.keeper.homepage.global.error.ErrorCode.SEMINAR_ATTENDANCE_CODE_NOT_AVAILABLE; import static com.keeper.homepage.global.error.ErrorCode.SEMINAR_ATTENDANCE_DUPLICATE; -import static com.keeper.homepage.global.error.ErrorCode.SEMINAR_NOT_FOUND; +import static com.keeper.homepage.global.error.ErrorCode.SEMINAR_ATTENDANCE_UNABLE; +import com.keeper.homepage.domain.member.application.convenience.MemberFindService; import com.keeper.homepage.domain.member.entity.Member; -import com.keeper.homepage.domain.member.entity.embedded.Nickname; +import com.keeper.homepage.domain.seminar.application.convenience.ValidSeminarFindService; import com.keeper.homepage.domain.seminar.dao.SeminarAttendanceRepository; -import com.keeper.homepage.domain.seminar.dao.SeminarRepository; import com.keeper.homepage.domain.seminar.dto.request.SeminarAttendanceCodeRequest; import com.keeper.homepage.domain.seminar.dto.request.SeminarAttendanceStatusRequest; import com.keeper.homepage.domain.seminar.dto.response.SeminarAttendanceResponse; import com.keeper.homepage.domain.seminar.entity.Seminar; import com.keeper.homepage.domain.seminar.entity.SeminarAttendance; import com.keeper.homepage.global.error.BusinessException; +import com.keeper.homepage.global.util.redis.RedisUtil; +import java.time.LocalDate; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -24,51 +28,61 @@ @Transactional(readOnly = true) public class SeminarAttendanceService { + private final RedisUtil redisUtil; + private final ValidSeminarFindService validSeminarFindService; + private final MemberFindService memberFindService; private final SeminarAttendanceRepository attendanceRepository; - private final SeminarRepository seminarRepository; + + private static final int MAX_ATTEMPT_COUNT = 5; + private static final int ONE_HOUR = 60 * 60; @Transactional - public SeminarAttendanceResponse save(Long seminarId, Member member, SeminarAttendanceCodeRequest request){ - Seminar seminar = seminarRepository.findById(seminarId) - .orElseThrow(() -> new BusinessException(seminarId, "seminarId", SEMINAR_NOT_FOUND)); + public SeminarAttendanceResponse attendance(Long seminarId, Member member, SeminarAttendanceCodeRequest request) { + Seminar seminar = validSeminarFindService.findById(seminarId); + SeminarAttendance seminarAttendance = attendanceRepository.findBySeminarAndMember(seminar, member) + .orElseThrow(() -> new BusinessException(member.getRealName(), "member", SEMINAR_ATTENDANCE_UNABLE)); + + String key = "seminar:" + LocalDate.now() + ":memberId:" + member.getId(); + checkAttemptNumberLimit(key); - validAttendanceCode(seminar, request); - checkExistsAttendanceSeminar(seminar, member); + validAttendanceCode(seminar, request, key); + checkDuplicateAttendance(seminarAttendance); - SeminarAttendance attendance = SeminarAttendance.builder() - .seminar(seminar) - .member(member) - .seminarAttendanceStatus(seminar.getStatus()) - .build(); + seminarAttendance.changeStatus(seminar.getStatus().getType()); + return SeminarAttendanceResponse.of(seminarAttendance); + } - return SeminarAttendanceResponse.from(attendanceRepository.save(attendance)); + private void checkAttemptNumberLimit(String key) { + int attemptNumber = redisUtil.increaseAndGetWithExpire(key, ONE_HOUR).intValue(); + if (attemptNumber > MAX_ATTEMPT_COUNT) { + throw new BusinessException(attemptNumber, "attemptNumber", SEMINAR_ATTENDANCE_ATTEMPT_NOT_AVAILABLE); + } } - private void validAttendanceCode(Seminar seminar, SeminarAttendanceCodeRequest request) { + private void validAttendanceCode(Seminar seminar, SeminarAttendanceCodeRequest request, String key) { String seminarAttendanceCode = seminar.getAttendanceCode(); String inputAttendanceCode = request.attendanceCode(); if (!seminarAttendanceCode.equals(inputAttendanceCode)) { - throw new BusinessException(inputAttendanceCode, "inputAttendanceCode", - SEMINAR_ATTENDANCE_CODE_NOT_AVAILABLE); + String attemptNumber = redisUtil.getData(key, String.class).orElseThrow(); + throw new BusinessException(attemptNumber, "attemptNumber", SEMINAR_ATTENDANCE_CODE_NOT_AVAILABLE); } } - private void checkExistsAttendanceSeminar(Seminar seminar, Member member) { - if (attendanceRepository.existsBySeminarAndMember(seminar, member)) { - throw new BusinessException(seminar.getName(), "seminar", SEMINAR_ATTENDANCE_DUPLICATE); + private void checkDuplicateAttendance(SeminarAttendance seminarAttendance) { + if (seminarAttendance.getSeminarAttendanceStatus().getType() != BEFORE_ATTENDANCE) { + throw new BusinessException(seminarAttendance.getSeminarAttendanceStatus(), "attendanceStatus", + SEMINAR_ATTENDANCE_DUPLICATE); } } @Transactional - public void changeStatus(Long seminarId, Member member, SeminarAttendanceStatusRequest request) { - Nickname nickname = member.getProfile().getNickname(); - - Seminar seminar = seminarRepository.findById(seminarId) - .orElseThrow(() -> new BusinessException(seminarId, "seminarId", SEMINAR_NOT_FOUND)); + public void changeStatus(long seminarId, long memberId, SeminarAttendanceStatusRequest request) { + Member member = memberFindService.findById(memberId); + Seminar seminar = validSeminarFindService.findById(seminarId); SeminarAttendance seminarAttendance = attendanceRepository.findBySeminarAndMember(seminar, member) - .orElseThrow(() -> new BusinessException(nickname, "nickname", MEMBER_NOT_FOUND)); + .orElseThrow(() -> new BusinessException(member.getNickname(), "nickname", MEMBER_NOT_FOUND)); seminarAttendance.changeStatus(request.excuse(), request.statusType()); } diff --git a/src/main/java/com/keeper/homepage/domain/seminar/application/SeminarService.java b/src/main/java/com/keeper/homepage/domain/seminar/application/SeminarService.java index f87f0ce37..c04abd938 100644 --- a/src/main/java/com/keeper/homepage/domain/seminar/application/SeminarService.java +++ b/src/main/java/com/keeper/homepage/domain/seminar/application/SeminarService.java @@ -1,21 +1,28 @@ package com.keeper.homepage.domain.seminar.application; -import static com.keeper.homepage.global.error.ErrorCode.SEMINAR_NOT_FOUND; +import static com.keeper.homepage.domain.seminar.entity.SeminarAttendanceStatus.SeminarAttendanceStatusType.BEFORE_ATTENDANCE; +import static com.keeper.homepage.domain.seminar.entity.SeminarAttendanceStatus.getSeminarAttendanceStatusBy; import static com.keeper.homepage.global.error.ErrorCode.SEMINAR_TIME_NOT_AVAILABLE; import static java.util.stream.Collectors.joining; +import com.keeper.homepage.domain.member.application.convenience.MemberFindService; +import com.keeper.homepage.domain.member.entity.Member; import com.keeper.homepage.domain.seminar.application.convenience.ValidSeminarFindService; +import com.keeper.homepage.domain.seminar.dao.SeminarAttendanceRepository; import com.keeper.homepage.domain.seminar.dao.SeminarRepository; import com.keeper.homepage.domain.seminar.dto.request.SeminarStartRequest; import com.keeper.homepage.domain.seminar.dto.response.SeminarAttendanceCodeResponse; +import com.keeper.homepage.domain.seminar.dto.response.SeminarDetailResponse; import com.keeper.homepage.domain.seminar.dto.response.SeminarIdResponse; import com.keeper.homepage.domain.seminar.dto.response.SeminarListResponse; import com.keeper.homepage.domain.seminar.dto.response.SeminarResponse; import com.keeper.homepage.domain.seminar.entity.Seminar; +import com.keeper.homepage.domain.seminar.entity.SeminarAttendance; import com.keeper.homepage.global.error.BusinessException; import com.keeper.homepage.global.error.ErrorCode; import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.List; import java.util.Random; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -29,13 +36,25 @@ public class SeminarService { private static final Random RANDOM = new Random(); private final SeminarRepository seminarRepository; + private final SeminarAttendanceRepository seminarAttendanceRepository; private final ValidSeminarFindService validSeminarFindService; + private final MemberFindService memberFindService; @Transactional public SeminarIdResponse save() { Seminar seminar = Seminar.builder() .attendanceCode(randomAttendanceCode()) .build(); + + List activeMembers = memberFindService.findAllRegular(); + activeMembers.forEach(member -> { + seminarAttendanceRepository.save( + SeminarAttendance.builder() + .seminar(seminar) + .member(member) + .seminarAttendanceStatus(getSeminarAttendanceStatusBy(BEFORE_ATTENDANCE)) + .build()); + }); return new SeminarIdResponse(seminarRepository.save(seminar).getId()); } @@ -51,8 +70,7 @@ private String randomAttendanceCode() { public SeminarAttendanceCodeResponse start(Long seminarId, SeminarStartRequest request) { validCloseTime(request); - Seminar seminar = seminarRepository.findById(seminarId) - .orElseThrow(() -> new BusinessException(seminarId, "seminarId", SEMINAR_NOT_FOUND)); + Seminar seminar = validSeminarFindService.findById(seminarId); seminar.changeCloseTime(request.attendanceCloseTime(), request.latenessCloseTime()); return new SeminarAttendanceCodeResponse(seminar.getAttendanceCode()); } @@ -82,14 +100,20 @@ public static void requireNonNull(T obj, String fieldName, ErrorCode errorCo @Transactional public void delete(long seminarId) { - Seminar seminar = seminarRepository.findById(seminarId) - .orElseThrow(() -> new BusinessException(seminarId, "seminarId", SEMINAR_NOT_FOUND)); + Seminar seminar = validSeminarFindService.findById(seminarId); seminarRepository.delete(seminar); } - public SeminarResponse findById(long seminarId) { - return SeminarResponse.from(seminarRepository.findById(seminarId) - .orElseThrow(() -> new BusinessException(seminarId, "seminarId", SEMINAR_NOT_FOUND))); + public SeminarDetailResponse findById(Member member, long seminarId) { + Seminar seminar = validSeminarFindService.findById(seminarId); + + var seminarAttendanceStatusType = seminarAttendanceRepository.findBySeminarAndMember(seminar, member) + .orElse(SeminarAttendance.builder() + .seminarAttendanceStatus(getSeminarAttendanceStatusBy(BEFORE_ATTENDANCE)) + .build()) + .getSeminarAttendanceStatus() + .getType(); + return SeminarDetailResponse.from(seminar, seminarAttendanceStatusType); } public SeminarResponse findByAvailable() { @@ -106,4 +130,21 @@ public SeminarResponse findByDate(LocalDate localDate) { public SeminarListResponse findAll() { return new SeminarListResponse(validSeminarFindService.findAll()); } + + public SeminarIdResponse getRecentlyDoneSeminar() { + LocalDate now = LocalDate.now(); + return seminarRepository.findRecentlyDoneSeminar(now) + .map(Seminar::getId) + .map(SeminarIdResponse::new) + .orElse(null); + } + + public List getRecentlyUpcomingSeminars() { + LocalDate now = LocalDate.now(); + return seminarRepository.findRecentlyUpcomingSeminar(now) + .stream() + .map(Seminar::getId) + .map(SeminarIdResponse::new) + .toList(); + } } diff --git a/src/main/java/com/keeper/homepage/domain/seminar/application/convenience/ValidSeminarFindService.java b/src/main/java/com/keeper/homepage/domain/seminar/application/convenience/ValidSeminarFindService.java index bcac5f5c0..91cc4f384 100644 --- a/src/main/java/com/keeper/homepage/domain/seminar/application/convenience/ValidSeminarFindService.java +++ b/src/main/java/com/keeper/homepage/domain/seminar/application/convenience/ValidSeminarFindService.java @@ -1,7 +1,11 @@ package com.keeper.homepage.domain.seminar.application.convenience; +import static com.keeper.homepage.global.error.ErrorCode.SEMINAR_NOT_FOUND; + import com.keeper.homepage.domain.seminar.dao.SeminarRepository; import com.keeper.homepage.domain.seminar.dto.response.SeminarResponse; +import com.keeper.homepage.domain.seminar.entity.Seminar; +import com.keeper.homepage.global.error.BusinessException; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -20,4 +24,9 @@ public List findAll() { .map(SeminarResponse::from) .toList(); } + + public Seminar findById(long seminarId) { + return seminarRepository.findByIdAndIdNot(seminarId, VIRTUAL_SEMINAR_ID) + .orElseThrow(() -> new BusinessException(seminarId, "seminarId", SEMINAR_NOT_FOUND)); + } } diff --git a/src/main/java/com/keeper/homepage/domain/seminar/dao/SeminarRepository.java b/src/main/java/com/keeper/homepage/domain/seminar/dao/SeminarRepository.java index d56a03d44..9646c75b8 100644 --- a/src/main/java/com/keeper/homepage/domain/seminar/dao/SeminarRepository.java +++ b/src/main/java/com/keeper/homepage/domain/seminar/dao/SeminarRepository.java @@ -13,6 +13,8 @@ public interface SeminarRepository extends JpaRepository { List findAllByIdIsNot(Long seminarId); + Optional findByIdAndIdNot(Long seminarId, Long virtualId); + @Query("SELECT s FROM Seminar s WHERE DATE(s.openTime) =:localDate") Optional findByOpenTime(@Param("localDate") LocalDate localDate); @@ -21,4 +23,19 @@ public interface SeminarRepository extends JpaRepository { + "AND s.latenessCloseTime is not null " + "AND s.latenessCloseTime > :dateTime") Optional findByAvailable(@Param("dateTime") LocalDateTime dateTime); + + @Query("SELECT s FROM Seminar s " + + "WHERE s.id <> 1 " // virtual seminar data 제외 + + "AND DATE(s.openTime) < :localDate " + + "AND DATE(s.latenessCloseTime) < :localDate " + + "ORDER BY s.openTime DESC " + + "LIMIT 1") + Optional findRecentlyDoneSeminar(@Param("localDate") LocalDate localDate); + + @Query("SELECT s FROM Seminar s " + + "WHERE s.id <> 1 " // virtual seminar data 제외" + + "AND DATE(s.openTime) >= :localDate " + + "ORDER BY s.openTime ASC " + + "LIMIT 2") + List findRecentlyUpcomingSeminar(@Param("localDate") LocalDate localDate); } diff --git a/src/main/java/com/keeper/homepage/domain/seminar/dto/response/SeminarAttendanceResponse.java b/src/main/java/com/keeper/homepage/domain/seminar/dto/response/SeminarAttendanceResponse.java index 55aab65de..2d02bdeb6 100644 --- a/src/main/java/com/keeper/homepage/domain/seminar/dto/response/SeminarAttendanceResponse.java +++ b/src/main/java/com/keeper/homepage/domain/seminar/dto/response/SeminarAttendanceResponse.java @@ -6,10 +6,12 @@ import com.keeper.homepage.domain.seminar.entity.SeminarAttendance; import com.keeper.homepage.domain.seminar.entity.SeminarAttendanceStatus.SeminarAttendanceStatusType; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Getter +@Builder @NoArgsConstructor(access = PRIVATE) @AllArgsConstructor(access = PACKAGE) public class SeminarAttendanceResponse { @@ -17,8 +19,10 @@ public class SeminarAttendanceResponse { private Long id; private SeminarAttendanceStatusType statusType; - public static SeminarAttendanceResponse from(SeminarAttendance attendance) { - return new SeminarAttendanceResponse(attendance.getId(), - attendance.getSeminarAttendanceStatus().getType()); + public static SeminarAttendanceResponse of(SeminarAttendance attendance) { + return SeminarAttendanceResponse.builder() + .id(attendance.getId()) + .statusType(attendance.getSeminarAttendanceStatus().getType()) + .build(); } } diff --git a/src/main/java/com/keeper/homepage/domain/seminar/dto/response/SeminarDetailResponse.java b/src/main/java/com/keeper/homepage/domain/seminar/dto/response/SeminarDetailResponse.java new file mode 100644 index 000000000..28773b24e --- /dev/null +++ b/src/main/java/com/keeper/homepage/domain/seminar/dto/response/SeminarDetailResponse.java @@ -0,0 +1,35 @@ +package com.keeper.homepage.domain.seminar.dto.response; + +import com.keeper.homepage.domain.seminar.entity.Seminar; +import com.keeper.homepage.domain.seminar.entity.SeminarAttendance; +import com.keeper.homepage.domain.seminar.entity.SeminarAttendanceStatus; +import com.keeper.homepage.domain.seminar.entity.SeminarAttendanceStatus.SeminarAttendanceStatusType; +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class SeminarDetailResponse { + + private Long seminarId; + private String seminarName; + private LocalDateTime openTime; + private LocalDateTime attendanceCloseTime; + private LocalDateTime latenessCloseTime; + private String statusType; + + public static SeminarDetailResponse from(Seminar seminar, SeminarAttendanceStatusType seminarAttendanceStatusType) { + return SeminarDetailResponse.builder() + .seminarId(seminar.getId()) + .seminarName(seminar.getName()) + .openTime(seminar.getOpenTime()) + .attendanceCloseTime(seminar.getAttendanceCloseTime()) + .latenessCloseTime(seminar.getLatenessCloseTime()) + .statusType(seminarAttendanceStatusType.getType()) + .build(); + } +} diff --git a/src/main/java/com/keeper/homepage/domain/seminar/entity/SeminarAttendance.java b/src/main/java/com/keeper/homepage/domain/seminar/entity/SeminarAttendance.java index 6d0e1df6d..7399b50c2 100644 --- a/src/main/java/com/keeper/homepage/domain/seminar/entity/SeminarAttendance.java +++ b/src/main/java/com/keeper/homepage/domain/seminar/entity/SeminarAttendance.java @@ -76,6 +76,10 @@ public void changeStatus(String excuse, SeminarAttendanceStatusType type) { } } + public void changeStatus(SeminarAttendanceStatusType type) { + seminarAttendanceStatus = getSeminarAttendanceStatusBy(type); + } + @Builder private SeminarAttendance(Seminar seminar, Member member, SeminarAttendanceStatus seminarAttendanceStatus, LocalDateTime attendTime) { diff --git a/src/main/java/com/keeper/homepage/global/error/ErrorCode.java b/src/main/java/com/keeper/homepage/global/error/ErrorCode.java index 251a23313..1bebad1ca 100644 --- a/src/main/java/com/keeper/homepage/global/error/ErrorCode.java +++ b/src/main/java/com/keeper/homepage/global/error/ErrorCode.java @@ -29,7 +29,9 @@ public enum ErrorCode { SEMINAR_NOT_FOUND("해당 세미나를 찾을 수 없습니다.", HttpStatus.NOT_FOUND), SEMINAR_TIME_NOT_AVAILABLE("올바르지 않은 시간 값입니다", HttpStatus.BAD_REQUEST), SEMINAR_ATTENDANCE_CODE_NOT_AVAILABLE("올바르지 않은 출석 코드입니다.", HttpStatus.BAD_REQUEST), + SEMINAR_ATTENDANCE_ATTEMPT_NOT_AVAILABLE("출석코드 입력 가능 횟수를 초과하여 출석이 불가능합니다.", HttpStatus.BAD_REQUEST), SEMINAR_ATTENDANCE_DUPLICATE("이미 출석을 완료한 계정입니다.", HttpStatus.CONFLICT), + SEMINAR_ATTENDANCE_UNABLE("활동회원이 아닌 회원은 출석이 불가합니다.", HttpStatus.BAD_REQUEST), // POST POST_CATEGORY_NOT_FOUND("존재하지 않는 게시글 카테고리입니다.", HttpStatus.NOT_FOUND), POST_NOT_FOUND("존재하지 않는 게시글입니다.", HttpStatus.NOT_FOUND), diff --git a/src/main/java/com/keeper/homepage/global/util/redis/RedisUtil.java b/src/main/java/com/keeper/homepage/global/util/redis/RedisUtil.java index 0b544eeb0..15ebefd14 100644 --- a/src/main/java/com/keeper/homepage/global/util/redis/RedisUtil.java +++ b/src/main/java/com/keeper/homepage/global/util/redis/RedisUtil.java @@ -47,6 +47,11 @@ public Long increaseAndGetWithExpire(String key, long durationMillis) { return valueOperations.increment(key); } + public Long increment(String key) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + return valueOperations.increment(key); + } + public void deleteData(String key) { redisTemplate.delete(key); } diff --git a/src/test/java/com/keeper/homepage/IntegrationTest.java b/src/test/java/com/keeper/homepage/IntegrationTest.java index 697ef7c7e..3c19d6ab5 100644 --- a/src/test/java/com/keeper/homepage/IntegrationTest.java +++ b/src/test/java/com/keeper/homepage/IntegrationTest.java @@ -64,6 +64,7 @@ import com.keeper.homepage.domain.post.dao.PostRepository; import com.keeper.homepage.domain.post.dao.category.CategoryRepository; import com.keeper.homepage.domain.seminar.SeminarTestHelper; +import com.keeper.homepage.domain.seminar.application.SeminarAttendanceService; import com.keeper.homepage.domain.seminar.application.SeminarService; import com.keeper.homepage.domain.seminar.application.convenience.ValidSeminarFindService; import com.keeper.homepage.domain.seminar.dao.SeminarAttendanceExcuseRepository; @@ -287,13 +288,16 @@ public class IntegrationTest { @SpykBean protected GameService gameService; - + @SpyBean protected MemberJobService memberJobService; - + @SpyBean protected AttendanceService attendanceService; + @SpyBean + protected SeminarAttendanceService seminarAttendanceService; + /******* Helper *******/ @SpyBean protected StaticWriteTestHelper staticWriteTestHelper; diff --git a/src/test/java/com/keeper/homepage/domain/member/MemberTestHelper.java b/src/test/java/com/keeper/homepage/domain/member/MemberTestHelper.java index 5a16a04ac..0ca6401e6 100644 --- a/src/test/java/com/keeper/homepage/domain/member/MemberTestHelper.java +++ b/src/test/java/com/keeper/homepage/domain/member/MemberTestHelper.java @@ -17,6 +17,7 @@ import com.keeper.homepage.domain.member.entity.job.MemberHasMemberJob; import com.keeper.homepage.domain.member.entity.job.MemberJob; import com.keeper.homepage.domain.member.entity.job.MemberJob.MemberJobType; +import com.keeper.homepage.domain.member.entity.type.MemberType; import com.keeper.homepage.domain.thumbnail.entity.Thumbnail; import com.keeper.homepage.global.config.security.JwtTokenProvider; import com.keeper.homepage.global.util.thumbnail.ThumbnailTestHelper; diff --git a/src/test/java/com/keeper/homepage/domain/seminar/api/SeminarApiTestHelper.java b/src/test/java/com/keeper/homepage/domain/seminar/api/SeminarApiTestHelper.java index 25c871f9b..d53a4e612 100644 --- a/src/test/java/com/keeper/homepage/domain/seminar/api/SeminarApiTestHelper.java +++ b/src/test/java/com/keeper/homepage/domain/seminar/api/SeminarApiTestHelper.java @@ -75,28 +75,31 @@ ResultActions deleteSeminarUsingApi(String token, Long seminarId) throws Excepti ResultActions attendanceSeminarUsingApi(String token, Long seminarId, SeminarAttendanceCodeRequest request) throws Exception { - return mockMvc.perform(post("/seminars/{seminarId}/attendances", seminarId) + return mockMvc.perform(patch("/seminars/{seminarId}/attendances", seminarId) .cookie(new Cookie(ACCESS_TOKEN.getTokenName(), token)) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); } ResultActions attendanceSeminarUsingApi(String token, Long seminarId, String strJson) throws Exception { - return mockMvc.perform(post("/seminars/{seminarId}/attendances", seminarId) + return mockMvc.perform(patch("/seminars/{seminarId}/attendances", seminarId) .cookie(new Cookie(ACCESS_TOKEN.getTokenName(), token)) .contentType(MediaType.APPLICATION_JSON) .content(strJson)); } - ResultActions changeAttendanceStatusUsingApi(String token, Long seminarId, SeminarAttendanceStatusRequest request) throws Exception { - return mockMvc.perform(patch("/seminars/{seminarId}/attendances", seminarId) + ResultActions changeAttendanceStatusUsingApi(String token, long seminarId, long memberId, + SeminarAttendanceStatusRequest request) + throws Exception { + return mockMvc.perform(patch("/seminars/{seminarId}/attendances/{memberId}", seminarId, memberId) .cookie(new Cookie(ACCESS_TOKEN.getTokenName(), token)) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); } - ResultActions changeAttendanceStatusUsingApi(String token, Long seminarId, String strJson) throws Exception { - return mockMvc.perform(patch("/seminars/{seminarId}/attendances", seminarId) + ResultActions changeAttendanceStatusUsingApi(String token, long seminarId, long memberId, String strJson) + throws Exception { + return mockMvc.perform(patch("/seminars/{seminarId}/attendances/{memberId}", seminarId, memberId) .cookie(new Cookie(ACCESS_TOKEN.getTokenName(), token)) .contentType(MediaType.APPLICATION_JSON) .content(strJson)); diff --git a/src/test/java/com/keeper/homepage/domain/seminar/api/SeminarAttendanceControllerTest.java b/src/test/java/com/keeper/homepage/domain/seminar/api/SeminarAttendanceControllerTest.java index bcf064881..170de95fc 100644 --- a/src/test/java/com/keeper/homepage/domain/seminar/api/SeminarAttendanceControllerTest.java +++ b/src/test/java/com/keeper/homepage/domain/seminar/api/SeminarAttendanceControllerTest.java @@ -14,12 +14,15 @@ import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.keeper.homepage.domain.seminar.dto.request.SeminarAttendanceCodeRequest; import com.keeper.homepage.domain.seminar.dto.request.SeminarAttendanceStatusRequest; import com.keeper.homepage.domain.seminar.dto.request.SeminarStartRequest; import com.keeper.homepage.domain.seminar.dto.response.SeminarAttendanceResponse; +import com.keeper.homepage.domain.seminar.entity.Seminar; import com.keeper.homepage.domain.seminar.entity.SeminarAttendanceStatus.SeminarAttendanceStatusType; import java.time.LocalDateTime; import org.junit.jupiter.api.BeforeEach; @@ -79,6 +82,10 @@ public void should_success_attendanceSeminar() throws Exception { requestCookies( cookieWithName(ACCESS_TOKEN.getTokenName()).description( "ACCESS TOKEN %s".formatted(securedValue))), + pathParameters( + parameterWithName("seminarId") + .description("세미나의 ID") + ), requestFields( field("attendanceCode", "세미나 출석 코드")), responseFields( @@ -122,16 +129,17 @@ public void should_fail_invalidInputValue(String attendanceCode) throws Exceptio Long seminarId = createSeminarAndGetId(adminToken); startSeminarUsingApi(adminToken, seminarId, seminarStartRequest).andExpect(status().isOk()); - attendanceSeminarUsingApi(adminToken, seminarId, strJson.formatted(attendanceCode)).andExpect(status().isBadRequest()); + attendanceSeminarUsingApi(adminToken, seminarId, strJson.formatted(attendanceCode)).andExpect( + status().isBadRequest()); } @Test @DisplayName("세미나 지각을 성공한다.") public void should_success_when_attendanceSeminarLateness() throws Exception { - Long seminarId = seminarTestHelper.builder() - .attendanceCloseTime(now.plusMinutes(-2)) - .latenessCloseTime(now.plusMinutes(3)) - .build().getId(); + long seminarId = seminarService.save().id(); + Seminar seminar = seminarRepository.findById(seminarId).orElseThrow(); + seminar.changeCloseTime(now.plusMinutes(-2), now.plusMinutes(3)); + String attendanceCode = seminarRepository.findById(seminarId).orElseThrow() .getAttendanceCode(); @@ -152,10 +160,10 @@ public void should_success_when_attendanceSeminarLateness() throws Exception { @Test @DisplayName("세미나 지각 마감 시간이 지나면 결석 처리한다.") public void should_success_when_attendanceSeminarAbsence() throws Exception { - Long seminarId = seminarTestHelper.builder() - .attendanceCloseTime(now.plusMinutes(-2)) - .latenessCloseTime(now.plusMinutes(-1)) - .build().getId(); + long seminarId = seminarService.save().id(); + Seminar seminar = seminarRepository.findById(seminarId).orElseThrow(); + seminar.changeCloseTime(now.plusMinutes(-2), now.plusMinutes(-1)); + String attendanceCode = seminarRepository.findById(seminarId).orElseThrow() .getAttendanceCode(); @@ -173,6 +181,7 @@ public void should_success_when_attendanceSeminarAbsence() throws Exception { assertThat(statusType).isEqualTo(ABSENCE); } } + @Nested @DisplayName("세미나 출석 상태 변경 테스트") class SeminarAttendanceStatusTest { @@ -196,14 +205,19 @@ public void should_success_when_changeSeminarAttendanceStatus() throws Exception .build(); startSeminarUsingApi(adminToken, seminarId, seminarStartRequest).andExpect(status().isOk()); - attendanceSeminarUsingApi(adminToken, seminarId, request).andExpect(status().isCreated()); - changeAttendanceStatusUsingApi(adminToken, seminarId, statusRequest) + changeAttendanceStatusUsingApi(adminToken, seminarId, adminId, statusRequest) .andExpect(status().isNoContent()) .andDo(document("change-attendance-seminar", requestCookies( cookieWithName(ACCESS_TOKEN.getTokenName()).description( "ACCESS TOKEN %s".formatted(securedValue))), + pathParameters( + parameterWithName("memberId") + .description("출석 상태를 변경하고자 하는 회원의 ID"), + parameterWithName("seminarId") + .description("세미나의 ID") + ), requestFields( field("excuse", "세미나 사유"), field("statusType", "출석 타입")) @@ -226,8 +240,8 @@ public void should_success_when_emptyExcuse(String excuse) throws Exception { .build(); startSeminarUsingApi(adminToken, seminarId, seminarStartRequest).andExpect(status().isOk()); - attendanceSeminarUsingApi(adminToken, seminarId, request).andExpect(status().isCreated()); - changeAttendanceStatusUsingApi(adminToken, seminarId, strJson.formatted(excuse)).andExpect(status().isNoContent()); + changeAttendanceStatusUsingApi(adminToken, seminarId, adminId, strJson.formatted(excuse)).andExpect( + status().isNoContent()); } @ParameterizedTest @@ -246,8 +260,8 @@ public void should_fail_when_invalidValue(String statusType) throws Exception { .build(); startSeminarUsingApi(adminToken, seminarId, seminarStartRequest).andExpect(status().isOk()); - attendanceSeminarUsingApi(adminToken, seminarId, request).andExpect(status().isCreated()); - changeAttendanceStatusUsingApi(adminToken, seminarId, strJson.formatted(statusType)).andExpect(status().isBadRequest()); + changeAttendanceStatusUsingApi(adminToken, seminarId, adminId, strJson.formatted(statusType)).andExpect( + status().isBadRequest()); } } } diff --git a/src/test/java/com/keeper/homepage/domain/seminar/api/SeminarControllerTest.java b/src/test/java/com/keeper/homepage/domain/seminar/api/SeminarControllerTest.java index c9f535a79..efb46a934 100644 --- a/src/test/java/com/keeper/homepage/domain/seminar/api/SeminarControllerTest.java +++ b/src/test/java/com/keeper/homepage/domain/seminar/api/SeminarControllerTest.java @@ -4,12 +4,16 @@ import static com.keeper.homepage.domain.member.entity.job.MemberJob.MemberJobType.ROLE_회원; import static com.keeper.homepage.domain.member.entity.job.MemberJob.MemberJobType.ROLE_회장; import static com.keeper.homepage.global.config.security.data.JwtType.ACCESS_TOKEN; -import static com.keeper.homepage.global.restdocs.RestDocsHelper.*; +import static com.keeper.homepage.global.restdocs.RestDocsHelper.dateFormat; +import static com.keeper.homepage.global.restdocs.RestDocsHelper.dateTimeFormat; +import static com.keeper.homepage.global.restdocs.RestDocsHelper.field; +import static com.keeper.homepage.global.restdocs.RestDocsHelper.getSecuredValue; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.is; import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; @@ -21,6 +25,7 @@ import com.keeper.homepage.domain.seminar.dto.request.SeminarStartRequest; import com.keeper.homepage.domain.seminar.dto.response.SeminarIdResponse; import com.keeper.homepage.domain.seminar.entity.Seminar; +import jakarta.servlet.http.Cookie; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -105,6 +110,10 @@ public void should_successStartSeminar_when_admin() throws Exception { requestCookies( cookieWithName(ACCESS_TOKEN.getTokenName()).description( "ACCESS TOKEN %s".formatted(securedValue))), + pathParameters( + parameterWithName("seminarId") + .description("세미나의 ID") + ), requestFields( field("attendanceCloseTime", "출석 마감 시간", dateTimeFormat()), field("latenessCloseTime", "지각 마감 시간", dateTimeFormat())), @@ -279,14 +288,12 @@ public void should_successSearchSeminarUsingId_when_admin() throws Exception { em.clear(); var responseSeminarDescriptors = new FieldDescriptor[]{ - field("id", "세미나 ID"), + field("seminarId", "세미나 ID"), field("openTime", "세미나 생성 시간"), field("attendanceCloseTime", "출석 마감 시간"), field("latenessCloseTime", "지각 마감 시간"), - field("attendanceCode", "출석 코드"), - field("name", "세미나명"), - field("registerTime", "DB 생성 시간"), - field("updateTime", "DB 업데이트 시간") + field("seminarName", "세미나명"), + field("statusType", "세미나 출석 상태") }; searchSeminarUsingApi(adminToken, seminarId).andExpect(status().isOk()) @@ -306,12 +313,6 @@ public void should_successSearchSeminarUsingId_when_admin() throws Exception { public void should_failSearchSeminarNotExistId_when_admin() throws Exception { searchSeminarUsingApi(adminToken, 0L).andExpect(status().isNotFound()); } - - @Test - @DisplayName("관리자 권한이 아니면 id 값으로 세미나 조회를 실패한다.") - public void should_failSearchSeminarUsingId_when_notAdmin() throws Exception { - searchSeminarUsingApi(userToken, 2L).andExpect(status().isForbidden()); - } } @Nested @@ -509,4 +510,65 @@ public void should_failDeleteNotExistsSeminar_when_admin() throws Exception { deleteSeminarUsingApi(adminToken, -1L).andExpect(status().isNotFound()); } } + + @Nested + @DisplayName("가장 최근에 마감된 세미나 조회 테스트") + class GetRecentlyDoneSeminarTest { + + @Test + @DisplayName("가장 최근에 마감된 세미나는 성공적으로 조회된다.") + public void 가장_최근에_마감된_세미나는_성공적으로_조회된다() throws Exception { + String securedValue = getSecuredValue(SeminarController.class, "getRecentlyDoneSeminar"); + + seminarTestHelper.builder() + .openTime(LocalDateTime.now().minusDays(1)) + .latenessCloseTime(LocalDateTime.now().minusDays(1)) + .build(); + + em.flush(); + em.clear(); + + mockMvc.perform(get("/seminars/recently-done") + .cookie(new Cookie(ACCESS_TOKEN.getTokenName(), userToken))) + .andExpect(status().isOk()) + .andDo(document("get-recently-done-seminar", + requestCookies( + cookieWithName(ACCESS_TOKEN.getTokenName()) + .description("ACCESS TOKEN %s".formatted(securedValue)) + ), + responseFields(field("id", "세미나 ID")) + )); + } + } + + @Nested + @DisplayName("가장 최근의 예정된 세미나 조회 테스트") + class GetRecentlyUpcomingSeminarTest { + + @Test + @DisplayName("유효한 요청일 경우 가장 최근의 예정된 세미나는 성공적으로 조회된다.") + public void 유효한_요청일_경우_가장_최근의_예정된_세미나는_성공적으로_조회된다() throws Exception { + String securedValue = getSecuredValue(SeminarController.class, "getRecentlyUpcomingSeminars"); + + seminarTestHelper.builder() + .openTime(LocalDateTime.now()) + .build(); + seminarTestHelper.builder() + .openTime(LocalDateTime.now().plusDays(1)) + .build(); + em.flush(); + em.clear(); + + mockMvc.perform(get("/seminars/recently-upcoming") + .cookie(new Cookie(ACCESS_TOKEN.getTokenName(), userToken))) + .andExpect(status().isOk()) + .andDo(document("get-recently-upcoming-seminars", + requestCookies( + cookieWithName(ACCESS_TOKEN.getTokenName()) + .description("ACCESS TOKEN %s".formatted(securedValue)) + ), + responseFields(field("[].id", "세미나 ID")) + )); + } + } } diff --git a/src/test/java/com/keeper/homepage/domain/seminar/application/SeminarAttendanceServiceTest.java b/src/test/java/com/keeper/homepage/domain/seminar/application/SeminarAttendanceServiceTest.java new file mode 100644 index 000000000..058da4f27 --- /dev/null +++ b/src/test/java/com/keeper/homepage/domain/seminar/application/SeminarAttendanceServiceTest.java @@ -0,0 +1,43 @@ +package com.keeper.homepage.domain.seminar.application; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import com.keeper.homepage.IntegrationTest; +import com.keeper.homepage.domain.member.entity.Member; +import com.keeper.homepage.domain.seminar.dto.request.SeminarAttendanceCodeRequest; +import com.keeper.homepage.domain.seminar.entity.Seminar; +import com.keeper.homepage.global.error.BusinessException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class SeminarAttendanceServiceTest extends IntegrationTest { + + @Nested + @DisplayName("출석코드 입력 제한 테스트") + class AttemptLimitTest { + + + @Test + @DisplayName("출석코드 5회 초과 입력 시 출석이 불가능해야 한다.") + public void 출석코드_5회_초과_입력_시_출석이_불가능해야_한다() throws Exception { + Member member = memberTestHelper.generate(); + long seminarId = seminarService.save().id(); + Seminar seminar = seminarRepository.findById(seminarId).orElseThrow(); + + String invalidAttendanceCode = "12345"; + + for (int i = 0; i < 5; i++) { + assertThatThrownBy(() -> seminarAttendanceService.attendance(seminar.getId(), member, + new SeminarAttendanceCodeRequest(invalidAttendanceCode))) + .isInstanceOf(BusinessException.class) + .hasMessageContaining("올바르지 않은 출석 코드입니다."); + } + + assertThatThrownBy(() -> seminarAttendanceService.attendance(seminar.getId(), member, + new SeminarAttendanceCodeRequest(seminar.getAttendanceCode()))) + .isInstanceOf(BusinessException.class) + .hasMessageContaining("출석코드 입력 가능 횟수를 초과하여 출석이 불가능합니다."); + } + } +} diff --git a/src/test/java/com/keeper/homepage/domain/seminar/application/SeminarServiceTest.java b/src/test/java/com/keeper/homepage/domain/seminar/application/SeminarServiceTest.java index 52e32c584..f5420353c 100644 --- a/src/test/java/com/keeper/homepage/domain/seminar/application/SeminarServiceTest.java +++ b/src/test/java/com/keeper/homepage/domain/seminar/application/SeminarServiceTest.java @@ -3,8 +3,112 @@ import static org.assertj.core.api.Assertions.assertThat; import com.keeper.homepage.IntegrationTest; +import com.keeper.homepage.domain.seminar.dto.response.SeminarIdResponse; +import java.time.LocalDateTime; +import java.util.List; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -class SeminarServiceTest extends IntegrationTest { +public class SeminarServiceTest extends IntegrationTest { + + @Nested + @DisplayName("가장 최근에 마감된 세미나 조회 테스트") + class GetRecentlyDoneSeminarTest { + + + @Test + @DisplayName("지각 마감시간이 지난 세미나를 불러와야 한다.") + public void 지각_마감시간이_지난_세미나를_불러와야_한다() throws Exception { + //given + Long ongoingSeminarId = seminarTestHelper.builder() + .openTime(LocalDateTime.now().minusDays(2)) + .latenessCloseTime(LocalDateTime.now().plusDays(1)) + .build() + .getId(); + Long doneSeminarId = seminarTestHelper.builder() + .openTime(LocalDateTime.now().minusDays(1)) + .latenessCloseTime(LocalDateTime.now().minusDays(1)) + .build() + .getId(); + + //when + Long seminarId = seminarService.getRecentlyDoneSeminar().id(); + + //then + assertThat(seminarId).isEqualTo(doneSeminarId); + } + + @Test + @DisplayName("오늘 날짜의 세미나는 불러오면 안된다.") + public void 오늘_날짜의_세미나는_불러오면_안된다() throws Exception { + //given + Long yesterdaySeminarId = seminarTestHelper.builder() + .openTime(LocalDateTime.now().minusDays(1)) + .latenessCloseTime(LocalDateTime.now().minusDays(1)) + .build() + .getId(); + Long todaySeminarId = seminarTestHelper.builder() + .openTime(LocalDateTime.now()) + .latenessCloseTime(LocalDateTime.now()) + .build() + .getId(); + + //when + Long seminarId = seminarService.getRecentlyDoneSeminar().id(); + + //then + assertThat(seminarId).isEqualTo(yesterdaySeminarId); + } + + @Test + @DisplayName("마감된 세미나 중 시작시간 기준으로 현재와 가장 가까운 세미나를 불러와야 한다.") + public void 마감된_세미나_중_시작시간_기준으로_현재와_가장_가까운_세미나를_불러와야_한다() throws Exception { + seminarTestHelper.builder() + .openTime(LocalDateTime.now().minusDays(2)) + .latenessCloseTime(LocalDateTime.now().minusDays(2)) + .build(); + Long recentSeminarId = seminarTestHelper.builder() + .openTime(LocalDateTime.now().minusDays(1)) + .latenessCloseTime(LocalDateTime.now().minusDays(1)) + .build() + .getId(); + seminarTestHelper.builder() + .openTime(LocalDateTime.now().minusDays(3)) + .latenessCloseTime(LocalDateTime.now().minusDays(3)) + .build(); + + Long seminarId = seminarService.getRecentlyDoneSeminar().id(); + + assertThat(seminarId).isEqualTo(recentSeminarId); + } + } + + @Nested + @DisplayName("가장 최근의 예정된 세미나 조회 테스트") + class GetRecentlyUpcomingSeminarTest { + + @Test + @DisplayName("오늘을 포함하여 예정된 최신 세미나 2개를 조회해야 한다.") + public void 오늘을_포함하여_예정된_최신_세미나_2개를_조회해야_한다() throws Exception { + Long yesterdaySeminarId = seminarTestHelper.builder() + .openTime(LocalDateTime.now().minusDays(1)) + .build() + .getId(); + Long seminarId1 = seminarTestHelper.builder() + .openTime(LocalDateTime.now()) + .build() + .getId(); + Long seminarId2 = seminarTestHelper.builder() + .openTime(LocalDateTime.now().plusDays(1)) + .build() + .getId(); + + List recentlyUpcomingSeminars = seminarService.getRecentlyUpcomingSeminars(); + + assertThat(recentlyUpcomingSeminars).hasSize(2); + assertThat(recentlyUpcomingSeminars.get(0).id()).isEqualTo(seminarId1); + assertThat(recentlyUpcomingSeminars.get(1).id()).isEqualTo(seminarId2); + } + } }