Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
import com.challenge.domain.challenge.Challenge;
import com.challenge.domain.challenge.ChallengeQueryRepository;
import com.challenge.domain.challenge.ChallengeRepository;
import com.challenge.domain.challengeRecord.ChallengeRecord;
import com.challenge.domain.challengeRecord.ChallengeRecordRepository;
import com.challenge.domain.member.Member;
import com.challenge.domain.record.Record;
import com.challenge.domain.record.RecordRepository;
import com.challenge.utils.date.DateUtils;
import com.challenge.validator.DateValidator;
import lombok.RequiredArgsConstructor;
Expand All @@ -36,7 +36,7 @@ public class ChallengeService {

private final ChallengeRepository challengeRepository;
private final CategoryRepository categoryRepository;
private final RecordRepository recordRepository;
private final ChallengeRecordRepository challengeRecordRepository;

private final ChallengeValidator challengeValidator;
private final CategoryValidator categoryValidator;
Expand Down Expand Up @@ -75,36 +75,32 @@ public ChallengeResponse createChallenge(Member member, ChallengeCreateServiceRe
public ChallengeResponse achieveChallenge(Member member, Long challengeId,
ChallengeAchieveServiceRequest request) {
// validation
challengeValidator.challengeExistsBy(member, challengeId);
DateValidator.isLocalDateFormatter(request.getAchieveDate());
DateValidator.isBeforeOrEqualToTodayFrom(request.getAchieveDate());
validateChallengeAchieveOrCancel(member, challengeId, request.getAchieveDate());

Challenge challenge = challengeRepository.getReferenceById(challengeId);
challengeValidator.hasDuplicateRecordFor(challenge, DateUtils.toLocalDate(request.getAchieveDate()));

Record record = Record.achieve(challenge, request.getAchieveDate());
recordRepository.save(record);
challenge.addRecord(record);
ChallengeRecord challengeRecord = ChallengeRecord.achieve(challenge, request.getAchieveDate());
challengeRecordRepository.save(challengeRecord);

return ChallengeResponse.of(challenge);
}

@Transactional
public ChallengeResponse cancelChallenge(Member member, Long challengeId, ChallengeCancelServiceRequest request) {
// validation
challengeValidator.challengeExistsBy(member, challengeId);
DateValidator.isLocalDateFormatter(request.getCancelDate());
DateValidator.isBeforeOrEqualToTodayFrom(request.getCancelDate());
validateChallengeAchieveOrCancel(member, challengeId, request.getCancelDate());

Challenge challenge = challengeRepository.getReferenceById(challengeId);

// validation
Record record = recordValidator.hasRecordFor(challenge, DateUtils.toLocalDate(request.getCancelDate()));
ChallengeRecord challengeRecord = recordValidator.isLatestRecordSuccessfulBy(challenge,
DateUtils.toLocalDate(request.getCancelDate()));

// 기록 삭제
challenge.getRecords().remove(record);
// Record removedRecord = record.cancel(challenge, request.getCancelDate());
// recordRepository.save(removedRecord);
challenge.getChallengeRecords().remove(challengeRecord);
ChallengeRecord cancelRecord = ChallengeRecord.cancel(challenge, request.getCancelDate());
challengeRecordRepository.save(cancelRecord);

return ChallengeResponse.of(challenge);
}
Expand Down Expand Up @@ -132,4 +128,11 @@ public Long deleteChallenge(Member member, Long challengeId) {
return challengeId;
}

// 챌린지 달성 또는 취소 시 유효성 검사
private void validateChallengeAchieveOrCancel(Member member, Long challengeId, String actionDate) {
challengeValidator.challengeExistsBy(member, challengeId);
DateValidator.isLocalDateFormatter(actionDate);
DateValidator.isBeforeOrEqualToTodayFrom(actionDate);
}

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.challenge.api.service.record.response;

import com.challenge.domain.challenge.Challenge;
import com.challenge.domain.record.Record;
import com.challenge.domain.challengeRecord.ChallengeRecord;
import com.challenge.utils.date.DateUtils;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -19,15 +19,16 @@ private RecordResponse(List<String> successDates) {
}

public static RecordResponse of(Challenge challenge) {
List<Record> records = challenge.getRecords();
if (records.isEmpty()) {
List<ChallengeRecord> challengeRecords = challenge.getChallengeRecords();
if (challengeRecords.isEmpty()) {
return null;
}

return RecordResponse.builder()
.successDates(
records.stream()
.map(record -> DateUtils.toDayString(record.getSuccessDate()))
challengeRecords.stream()
.filter(ChallengeRecord::isSucceed)
.map(record -> DateUtils.toDayString(record.getRecordDate()))
.toList()
)
.build();
Expand Down
30 changes: 13 additions & 17 deletions src/main/java/com/challenge/api/validator/RecordValidator.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.challenge.api.validator;

import com.challenge.domain.challenge.Challenge;
import com.challenge.domain.record.Record;
import com.challenge.domain.record.RecordRepository;
import com.challenge.domain.challengeRecord.ChallengeRecord;
import com.challenge.domain.challengeRecord.ChallengeRecordQueryRepository;
import com.challenge.exception.ErrorCode;
import com.challenge.exception.GlobalException;
import lombok.RequiredArgsConstructor;
Expand All @@ -16,21 +16,17 @@
@RequiredArgsConstructor
public class RecordValidator {

private final RecordRepository recordRepository;

/**
* 특정 챌린지와 날짜에 해당하는 레코드가 존재하는지 검증하고 반환
*
* @param challenge 검증 대상 챌린지
* @param cancelDate 취소하려는 날짜
* @return 검증된 Record 엔티티
* @throws GlobalException RECORD_NOT_FOUND 예외
*/
public Record hasRecordFor(Challenge challenge, LocalDate cancelDate) {
return challenge.getRecords().stream()
.filter(r -> r.getSuccessDate().equals(cancelDate))
.findFirst()
.orElseThrow(() -> new GlobalException(ErrorCode.RECORD_NOT_FOUND));
private final ChallengeRecordQueryRepository challengeRecordQueryRepository;

public ChallengeRecord isLatestRecordSuccessfulBy(Challenge challenge, LocalDate cancelDate) {
ChallengeRecord challengeRecord = challengeRecordQueryRepository.isLatestRecordSuccessfulBy(challenge,
cancelDate);

if (challengeRecord != null) {
return challengeRecord;
}

throw new GlobalException(ErrorCode.LATEST_ACHIEVE_RECORD_NOT_FOUND);
}

}
16 changes: 8 additions & 8 deletions src/main/java/com/challenge/domain/challenge/Challenge.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import com.challenge.api.service.challenge.request.ChallengeUpdateServiceRequest;
import com.challenge.domain.BaseDateTimeEntity;
import com.challenge.domain.category.Category;
import com.challenge.domain.challengeRecord.ChallengeRecord;
import com.challenge.domain.member.Member;
import com.challenge.domain.record.Record;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
Expand Down Expand Up @@ -36,7 +36,7 @@ public class Challenge extends BaseDateTimeEntity {
private Long id;

@OneToMany(mappedBy = "challenge", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<Record> records = new ArrayList<>();
private List<ChallengeRecord> challengeRecords = new ArrayList<>();

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
Expand Down Expand Up @@ -65,7 +65,7 @@ public class Challenge extends BaseDateTimeEntity {
private String color;

@Column(nullable = false)
private boolean isDeleted;
private boolean isDeleted = false;

@Column(nullable = false)
private LocalDateTime startDateTime;
Expand All @@ -75,8 +75,8 @@ public class Challenge extends BaseDateTimeEntity {

@Builder
private Challenge(Member member, Category category, String title, String content, int durationInWeeks,
int weeklyGoalCount, String color, LocalDateTime startDateTime) {
this.records = new ArrayList<>();
int weeklyGoalCount, String color, boolean isDeleted, LocalDateTime startDateTime) {
this.challengeRecords = new ArrayList<>();
this.member = member;
this.category = category;
this.title = title;
Expand All @@ -85,7 +85,7 @@ private Challenge(Member member, Category category, String title, String content
this.weeklyGoalCount = weeklyGoalCount;
this.totalGoalCount = durationInWeeks * weeklyGoalCount;
this.color = color;
this.isDeleted = false;
this.isDeleted = isDeleted;
this.startDateTime = startDateTime;
this.endDateTime = startDateTime.plusWeeks(durationInWeeks)
.toLocalDate()
Expand Down Expand Up @@ -115,8 +115,8 @@ public void update(Category category, ChallengeUpdateServiceRequest request) {
this.content = request.getContent();
}

public void addRecord(Record record) {
this.records.add(record);
public void addRecord(ChallengeRecord challengeRecord) {
this.challengeRecords.add(challengeRecord);
}

public void delete() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.challenge.domain.member.Member;
import com.challenge.utils.date.DateUtils;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
Expand All @@ -12,7 +11,7 @@
import java.util.List;

import static com.challenge.domain.challenge.QChallenge.challenge;
import static com.challenge.domain.record.QRecord.record;
import static com.challenge.domain.challengeRecord.QChallengeRecord.challengeRecord;

@RequiredArgsConstructor
@Repository
Expand All @@ -35,10 +34,11 @@ public List<Challenge> findChallengesBy(Member member, LocalDate targetDate) {

// 중복된 기록이 존재하는지 조회
public boolean existsDuplicateRecordBy(Challenge challenge, LocalDate successDate) {
Long count = queryFactory.select(record.count())
.from(record)
.where(record.challenge.eq(challenge)
.and(record.successDate.eq(successDate)))
Long count = queryFactory.select(challengeRecord.count())
.from(challengeRecord)
.where(challengeRecord.challenge.eq(challenge)
.and(challengeRecord.recordDate.eq(successDate))
.and(challengeRecord.isSucceed.eq(true)))
.fetchOne();

return count != null && count > 0;
Expand Down Expand Up @@ -70,17 +70,7 @@ public Long countOngoingChallengesBy(Member member) {

// 완료된 챌린지 수 조회
public Long countCompletedChallengesBy(Member member) {
return queryFactory
.select(challenge.count())
.from(challenge)
.leftJoin(record)
.on(record.challenge.eq(challenge)
.and(record.isSucceed.isTrue())
)
.where(challenge.member.eq(member))
.groupBy(challenge)
.having(record.count().eq(Expressions.constant(challenge.totalGoalCount)))
.fetchOne();
return null;
}

// 전체 챌린지 수 조회
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/challenge/domain/challenge/ChallengeStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.challenge.domain.challenge;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Getter
public enum ChallengeStatus {

// - Q. 챌린지 기간이 만료되는 경우, 어떻게 자동으로 챌린지의 상태를 변경할 수 있을까?
// A. 스케줄러 라이브러리를 사용하여 매 00시 00분 00초에 챌린지의 상태를 확인하여 챌린지 기간이 만료된 경우 상태를 변경한다.

// - Q. 챌린지의 상태를 변경하는 로직은 어디에 위치하는 것이 좋을까?
// A. 챌린지의 상태를 변경하는 로직은 챌린지 도메인에 위치하는 것이 좋다.

ONGOING("진행 중"),
SUCCEED("성공"), // 모든 목표를 달성한 경우
FAILED("실패"), // 하나라도 목표를 달성하지 못한 경우
REMOVED("삭제");

private final String description;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.challenge.domain.record;
package com.challenge.domain.challengeRecord;

import com.challenge.domain.BaseDateTimeEntity;
import com.challenge.domain.challenge.Challenge;
Expand All @@ -21,43 +21,43 @@
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Record extends BaseDateTimeEntity {
public class ChallengeRecord extends BaseDateTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "record_id")
@Column(name = "challenge_record_id")
private Long id;

@Column(nullable = false)
private LocalDate successDate;

@Column(nullable = false)
private boolean isSucceed = false;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "challenge_id", nullable = false)
private Challenge challenge;

@Column(nullable = false)
private LocalDate recordDate;

@Column(nullable = false)
private boolean isSucceed;

@Builder
private Record(LocalDate successDate, boolean isSucceed, Challenge challenge) {
this.successDate = successDate;
private ChallengeRecord(LocalDate recordDate, boolean isSucceed, Challenge challenge) {
this.recordDate = recordDate;
this.challenge = challenge;
this.isSucceed = isSucceed;
}

public static Record achieve(Challenge challenge, String achieveDate) {
Record record = Record.builder()
.successDate(DateUtils.toLocalDate(achieveDate))
public static ChallengeRecord achieve(Challenge challenge, String achieveDate) {
ChallengeRecord achieveRecord = ChallengeRecord.builder()
.recordDate(DateUtils.toLocalDate(achieveDate))
.isSucceed(true)
.challenge(challenge)
.build();
challenge.addRecord(record);
return record;
challenge.addRecord(achieveRecord);
return achieveRecord;
}

public Record cancel(Challenge challenge, String cancelDate) {
return Record.builder()
.successDate(DateUtils.toLocalDate(cancelDate))
public static ChallengeRecord cancel(Challenge challenge, String cancelDate) {
return ChallengeRecord.builder()
.recordDate(DateUtils.toLocalDate(cancelDate))
.isSucceed(false)
.challenge(challenge)
.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.challenge.domain.challengeRecord;

import com.challenge.domain.challenge.Challenge;
import com.challenge.exception.ErrorCode;
import com.challenge.exception.GlobalException;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.time.LocalDate;

import static com.challenge.domain.challengeRecord.QChallengeRecord.challengeRecord;

@RequiredArgsConstructor
@Repository
public class ChallengeRecordQueryRepository {

private final JPAQueryFactory queryFactory;

public ChallengeRecord isLatestRecordSuccessfulBy(Challenge challenge, LocalDate cancelDate) {
ChallengeRecord latestRecord = queryFactory.selectFrom(challengeRecord)
.where(challengeRecord.challenge.eq(challenge)
.and(challengeRecord.recordDate.eq(cancelDate))
)
.orderBy(challengeRecord.createdAt.desc())
.fetchFirst();

if (latestRecord != null && latestRecord.isSucceed()) {
return latestRecord;
}

throw new GlobalException(ErrorCode.LATEST_ACHIEVE_RECORD_NOT_FOUND);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.challenge.domain.challengeRecord;

import org.springframework.data.jpa.repository.JpaRepository;

public interface ChallengeRecordRepository extends JpaRepository<ChallengeRecord, Long> {

}
Loading
Loading