Skip to content

Commit

Permalink
[BE] issue188: 스터디 상태 자동 변경 (#191)
Browse files Browse the repository at this point in the history
* feat: 스케쥴러를 이용한 스터디 상태 자동 변경

* refactor: 스케줄러 로직 개선

* fix: 테스트 코드 수정

* refactor: 응답 값이 없는 경우 null 반환

* refactor: 메소드명 개선 및 Getter 추가

* refactor: 불필요한 메소드 제거

* refactor: log 개선

* refactor: 기존 메소드 활용

* refactor: `@Enumerated` 통일

* feat: 테스트 코드 수정

* refactor: StudyPlanner 수정

* feat: Study 불변식 검증 추가

* refactor: Auto에 Transactional 제거

* fix: 불필요한 Bean 제거

* test: 테스트 코드 개선
  • Loading branch information
tco0427 committed Aug 4, 2022
1 parent f25bec4 commit d918cc9
Show file tree
Hide file tree
Showing 24 changed files with 274 additions and 193 deletions.
Expand Up @@ -9,6 +9,7 @@
import com.woowacourse.moamoa.common.exception.UnauthorizedException;
import com.woowacourse.moamoa.study.domain.exception.InvalidPeriodException;
import com.woowacourse.moamoa.study.service.exception.FailureParticipationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
Expand All @@ -17,6 +18,7 @@
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

@RestControllerAdvice
@Slf4j
public class CommonControllerAdvice {

@ExceptionHandler({
Expand All @@ -34,22 +36,25 @@ public ResponseEntity<ErrorResponse> handleBadRequest() {
FailureParticipationException.class
})
public ResponseEntity<ErrorResponse> handleBadRequest(final Exception e) {
log.error("HandleBadRequest : {}", e.getMessage());
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
}

@ExceptionHandler({UnauthorizedException.class})
@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<Void> handleUnauthorized(final Exception e) {
log.error("UnauthorizedException : {}", e.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}

@ExceptionHandler({NotFoundException.class})
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(final Exception e) {
log.error("NotFoundException : {}", e.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(e.getMessage()));
}

@ExceptionHandler(RuntimeException.class)
public ResponseEntity<ErrorResponse> handleInternalServerError(RuntimeException e) {
e.printStackTrace();
log.error("RuntimeException : {}", e.getMessage());
return ResponseEntity.status(INTERNAL_SERVER_ERROR).body(new ErrorResponse("요청을 처리할 수 없습니다."));
}
}
@@ -1,26 +1,29 @@
package com.woowacourse.moamoa.study.domain;

import static com.woowacourse.moamoa.study.domain.RecruitStatus.RECRUITMENT_END;
import static com.woowacourse.moamoa.study.domain.RecruitStatus.RECRUITMENT_START;
import static javax.persistence.EnumType.STRING;
import static lombok.AccessLevel.PROTECTED;

import java.time.LocalDate;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import lombok.NoArgsConstructor;

@Embeddable
@NoArgsConstructor(access = PROTECTED)
public class RecruitPlanner {

@Column(name = "max_member_count")
private Integer max;

@Enumerated(EnumType.STRING)
@Enumerated(STRING)
@Column(name = "recruitment_status")
private RecruitStatus recruitStatus;

private LocalDate enrollmentEndDate;

protected RecruitPlanner() {
}

public RecruitPlanner(final Integer max, final RecruitStatus recruitStatus, final LocalDate enrollmentEndDate) {
this.max = max;
this.recruitStatus = recruitStatus;
Expand All @@ -38,20 +41,26 @@ boolean isRecruitedBeforeThan(LocalDate date) {
return enrollmentEndDate.isBefore(date);
}

boolean isNeedToCloseRecruiting(final LocalDate now) {
return recruitStatus.equals(RecruitStatus.RECRUITMENT_START) && isRecruitedBeforeThan(now);
void updateRecruiting(final LocalDate now) {
if (isNeedToCloseRecruiting(now)) {
closeRecruiting();
}
}

private boolean isNeedToCloseRecruiting(final LocalDate now) {
return recruitStatus.equals(RECRUITMENT_START) && isRecruitedBeforeThan(now);
}

void closeRecruiting() {
recruitStatus = RecruitStatus.RECRUITMENT_END;
recruitStatus = RECRUITMENT_END;
}

LocalDate getEnrollmentEndDate() {
return enrollmentEndDate;
}

boolean isCloseEnrollment() {
return recruitStatus.equals(RecruitStatus.RECRUITMENT_END);
return recruitStatus.equals(RECRUITMENT_END);
}

int getCapacity() {
Expand Down
Expand Up @@ -57,6 +57,10 @@ private Study(final Long id, final Content content, final Participants participa
throw new InvalidPeriodException();
}

if (studyPlanner.isInappropriateCondition(createdAt.toLocalDate())) {
throw new InvalidPeriodException();
}

this.id = id;
this.content = content;
this.participants = participants;
Expand Down Expand Up @@ -95,19 +99,20 @@ public void participate(final Long memberId) {
}
}

private boolean isFullOfCapacity() {
return recruitPlanner.hasCapacity() && recruitPlanner.getCapacity() == participants.getSize();
public void changeStatus(final LocalDate now) {
recruitPlanner.updateRecruiting(now);
studyPlanner.updateStatus(now);
}

public boolean isNeedToCloseRecruiting(LocalDate now) {
return recruitPlanner.isNeedToCloseRecruiting(now);
public boolean isProgressStatus() {
return studyPlanner.isProgress();
}

public void closeEnrollment() {
recruitPlanner.closeRecruiting();
public boolean isCloseStudy() {
return studyPlanner.isCloseStudy();
}

public boolean isCloseEnrollment() {
return recruitPlanner.isCloseEnrollment();
private boolean isFullOfCapacity() {
return recruitPlanner.hasCapacity() && recruitPlanner.getCapacity() == participants.getSize();
}
}
@@ -1,21 +1,25 @@
package com.woowacourse.moamoa.study.domain;

import static com.woowacourse.moamoa.study.domain.StudyStatus.DONE;
import static com.woowacourse.moamoa.study.domain.StudyStatus.IN_PROGRESS;
import static com.woowacourse.moamoa.study.domain.StudyStatus.PREPARE;
import static javax.persistence.EnumType.STRING;
import static lombok.AccessLevel.PROTECTED;

import com.woowacourse.moamoa.study.domain.exception.InvalidPeriodException;
import java.time.LocalDate;
import java.util.Objects;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.Enumerated;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

@Embeddable
@EqualsAndHashCode
@NoArgsConstructor(access = PROTECTED)
public class StudyPlanner {

@Enumerated(value = STRING)
@Enumerated(STRING)
@Column(nullable = false)
private StudyStatus studyStatus;

Expand Down Expand Up @@ -44,28 +48,43 @@ boolean isEndBeforeThan(LocalDate date) {
return endDate.isBefore(date);
}

void updateStatus(final LocalDate now) {
if (isNeedToChangeStatus(now)) {
studyStatus = studyStatus.nextStatus();
}
}

private boolean isNeedToChangeStatus(final LocalDate now) {
return isNeedToCloseStudy(now) || isNeedToChangeProgress(now);
}

private boolean isNeedToCloseStudy(final LocalDate now) {
return (endDate != null) && (isProgress()) && (endDate.isBefore(now) || endDate.isEqual(now));
}

private boolean isNeedToChangeProgress(final LocalDate now) {
return (isPreparing()) && (startDate.isBefore(now) || startDate.isEqual(now));
}

boolean isProgress() {
return studyStatus.equals(IN_PROGRESS);
}

boolean isPreparing() {
return studyStatus.equals(StudyStatus.PREPARE);
return studyStatus.equals(PREPARE);
}

public StudyStatus getStudyStatus() {
return studyStatus;
boolean isCloseStudy() {
return studyStatus.equals(DONE);
}

@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
public boolean isInappropriateCondition(final LocalDate createdDate) {
if (startDate.isEqual(createdDate) && studyStatus == IN_PROGRESS) {
return false;
}
if (o == null || getClass() != o.getClass()) {
if ((startDate.isAfter(createdDate)) && (studyStatus == PREPARE || studyStatus == IN_PROGRESS)) {
return false;
}
final StudyPlanner studyPlanner = (StudyPlanner) o;
return Objects.equals(startDate, studyPlanner.startDate) && Objects.equals(endDate, studyPlanner.endDate);
}

@Override
public int hashCode() {
return Objects.hash(startDate, endDate);
return !studyStatus.equals(DONE);
}
}
@@ -1,15 +1,16 @@
package com.woowacourse.moamoa.study.domain;

import java.util.Arrays;

public enum StudyStatus {

PREPARE, IN_PROGRESS, DONE;

public static StudyStatus find(String status) {
return Arrays.stream(StudyStatus.values())
.filter(studyStatus -> studyStatus.name().equals(status))
.findAny()
.get();
public StudyStatus nextStatus() {
if (this.equals(PREPARE)) {
return IN_PROGRESS;
}
if (this.equals(IN_PROGRESS)) {
return DONE;
}
return PREPARE;
}
}
Expand Up @@ -36,7 +36,7 @@ public class MyStudyDao {
final String startDate = rs.getString("start_date");
final String endDate = rs.getString("end_date");

return new MyStudySummaryData(id, title, StudyStatus.find(studyStatus),
return new MyStudySummaryData(id, title, StudyStatus.valueOf(studyStatus),
currentMemberCount, maxMemberCount, startDate, endDate);
};

Expand Down
@@ -1,34 +1,25 @@
package com.woowacourse.moamoa.study.schedule;

import com.woowacourse.moamoa.MoamoaApplication;
import com.woowacourse.moamoa.study.service.StudyService;
import java.time.LocalDateTime;
import java.time.ZoneId;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.config.TriggerTask;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
@Slf4j
public class AutoCloseEnrollmentTask extends TriggerTask {

private static final Logger LOGGER = LoggerFactory.getLogger(MoamoaApplication.class);

public AutoCloseEnrollmentTask(final StudyService studyService) {
super(runnable(studyService), new CronTrigger("@daily", ZoneId.of("Asia/Seoul")));
}

private static Runnable runnable(final StudyService studyService) {
return new Runnable() {

@Transactional
public void run() {
LOGGER.debug(LocalDateTime.now() + " : " + "start moamoa scheduled task!");
studyService.autoCloseStudies();
}
return () -> {
log.debug("{} : start moamoa scheduled task!", LocalDateTime.now());
studyService.autoUpdateStatus();
};
}
}
@@ -1,18 +1,15 @@
package com.woowacourse.moamoa.study.service;

import static com.woowacourse.moamoa.study.domain.StudyStatus.IN_PROGRESS;
import static com.woowacourse.moamoa.study.domain.StudyStatus.PREPARE;

import com.woowacourse.moamoa.common.exception.UnauthorizedException;
import com.woowacourse.moamoa.common.utils.DateTimeSystem;
import com.woowacourse.moamoa.member.domain.Member;
import com.woowacourse.moamoa.member.domain.repository.MemberRepository;
import com.woowacourse.moamoa.study.domain.AttachedTags;
import com.woowacourse.moamoa.study.domain.Content;
import com.woowacourse.moamoa.study.domain.Participants;
import com.woowacourse.moamoa.study.domain.StudyPlanner;
import com.woowacourse.moamoa.study.domain.RecruitPlanner;
import com.woowacourse.moamoa.study.domain.Study;
import com.woowacourse.moamoa.study.domain.StudyPlanner;
import com.woowacourse.moamoa.study.domain.repository.StudyRepository;
import com.woowacourse.moamoa.study.service.exception.StudyNotFoundException;
import com.woowacourse.moamoa.study.service.request.CreatingStudyRequest;
Expand Down Expand Up @@ -68,14 +65,12 @@ private Member findMemberBy(final Long githubId) {
.orElseThrow(() -> new UnauthorizedException(String.format("%d의 githubId를 가진 사용자는 없습니다.", githubId)));
}

public void autoCloseStudies() {
public void autoUpdateStatus() {
final List<Study> studies = studyRepository.findAll();
final LocalDate now = dateTimeSystem.now().toLocalDate();

for (Study study : studies) {
if (study.isNeedToCloseRecruiting(now)) {
study.closeEnrollment();
}
study.changeStatus(now);
}
}
}
Expand Up @@ -3,6 +3,7 @@
import com.woowacourse.moamoa.member.query.data.MemberData;
import com.woowacourse.moamoa.study.query.data.StudyDetailsData;
import com.woowacourse.moamoa.tag.query.response.TagData;
import java.time.LocalDate;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
Expand All @@ -22,11 +23,11 @@ public class StudyDetailResponse {
private String recruitmentStatus;
private String description;
private Integer currentMemberCount;
private String maxMemberCount;
private String createdDate;
private String enrollmentEndDate;
private String startDate;
private String endDate;
private Integer maxMemberCount;
private LocalDate createdDate;
private LocalDate enrollmentEndDate;
private LocalDate startDate;
private LocalDate endDate;
private MemberData owner;
private List<MemberData> members;
private List<TagData> tags;
Expand All @@ -41,20 +42,13 @@ public StudyDetailResponse(final StudyDetailsData study,
this.recruitmentStatus = study.getRecruitmentStatus();
this.description = study.getDescription();
this.currentMemberCount = study.getCurrentMemberCount();
this.maxMemberCount = getNullableDate(study.getMaxMemberCount());
this.createdDate = study.getCreatedDate().toString();
this.enrollmentEndDate = getNullableDate(study.getEnrollmentEndDate());
this.startDate = study.getStartDate().toString();
this.endDate = getNullableDate(study.getEndDate());
this.maxMemberCount = study.getMaxMemberCount();
this.createdDate = study.getCreatedDate();
this.enrollmentEndDate = study.getEnrollmentEndDate();
this.startDate = study.getStartDate();
this.endDate = study.getEndDate();
this.owner = study.getOwner();
this.members = participants;
this.tags = attachedTags;
}

private String getNullableDate(final Object value) {
if (value == null) {
return "";
}
return value.toString();
}
}

0 comments on commit d918cc9

Please sign in to comment.