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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/docs/asciidoc/seminar/seminar-attendance.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ link:../keeper.html[API 목록으로 돌아가기]

== *세미나 출석*

NOTE: 활동회원만 출석이 가능합니다.

=== 요청

==== Request
Expand All @@ -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[]
Expand All @@ -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[]
Expand Down
52 changes: 52 additions & 0 deletions src/docs/asciidoc/seminar/seminar.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down Expand Up @@ -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 문서 보기]
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -38,4 +41,11 @@ public Stream<Member> findAllByRealName(RealName realName) {
return memberRepository.findAllByProfileRealNameAndIdNot(realName, VIRTUAL_MEMBER_ID)
.stream();
}

public List<Member> findAllRegular() {
return memberRepository.findAllByMemberType(getMemberTypeBy(정회원))
.stream()
.filter(member -> member.getId() != VIRTUAL_MEMBER_ID)
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -35,4 +36,6 @@ Optional<Member> findByProfileEmailAddressAndProfileLoginId(EmailAddress profile
List<Member> findAllByProfileRealNameAndIdNot(RealName realName, long virtualId);

void deleteAllByIdNot(Long virtualId);

List<Member> findAllByMemberType(MemberType type);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,21 +23,21 @@ public class SeminarAttendanceController {

private final SeminarAttendanceService seminarAttendanceService;

@PostMapping
@PatchMapping
public ResponseEntity<SeminarAttendanceResponse> 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<Void> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -73,8 +77,26 @@ public ResponseEntity<SeminarResponse> getSeminarByDate(
}

@GetMapping("/{seminarId}")
public ResponseEntity<SeminarResponse> getSeminar(@PathVariable long seminarId) {
SeminarResponse response = seminarService.findById(seminarId);
@Secured({"ROLE_회원"})
public ResponseEntity<SeminarDetailResponse> 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<SeminarIdResponse> getRecentlyDoneSeminar() {
SeminarIdResponse response = seminarService.getRecentlyDoneSeminar();
return ResponseEntity.ok(response);
}

@GetMapping("/recently-upcoming")
@Secured({"ROLE_회원"})
public ResponseEntity<List<SeminarIdResponse>> getRecentlyUpcomingSeminars() {
List<SeminarIdResponse> responses = seminarService.getRecentlyUpcomingSeminars();
return ResponseEntity.ok(responses);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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());
}
Expand Down
Loading