Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
@Table(name = "routine_execution")
@Table(
name = "routine_execution",
uniqueConstraints = @UniqueConstraint(
name = "uk_routine_execution_routine_date",
columnNames = {"routine_id", "executed_date"}
)
)
public class RoutineExecution extends BaseEntity {

@Id
Expand Down Expand Up @@ -52,6 +58,10 @@ public void fail(Integer durationSecond) {
this.durationSecond = durationSecond;
}

public void recordActualWakeTime(LocalTime actualWakeTime) {
this.actualWakeTime = actualWakeTime;
}

public void recordInput(String memberInput) {
this.memberInput = memberInput;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@

import java.time.LocalDate;
import java.util.List;
import java.util.Optional;

public interface RoutineExecutionRepository extends JpaRepository<RoutineExecution, Long> {

Optional<RoutineExecution> findByRoutine_IdAndExecutedDate(Long routineId, LocalDate executedDate);

@Query("""
select distinct re.executedDate from RoutineExecution re
where re.routine.routineGroup.member.id = :memberId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
import com.moru.server.global.idempotency.IdempotencyService;
import com.moru.server.global.response.code.status.ErrorStatus;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;

import java.time.LocalDate;

@Service
@RequiredArgsConstructor
public class RoutineExecutionCommandServiceImpl implements RoutineExecutionCommandService {
Expand All @@ -41,20 +44,75 @@ public RoutineExecutionResponseDTO.RoutineExecutionResultRes saveExecutionResult
idempotencyKey,
req,
RoutineExecutionResponseDTO.RoutineExecutionResultRes.class,
() -> transactionTemplate.execute(status -> {
Routine routine = routineRepository.findById(req.routineId())
.orElseThrow(() -> new BusinessException(ErrorStatus.ROUTINE_NOT_FOUND));
() -> doSaveExecutionResult(memberId, req)
);
}

private RoutineExecutionResponseDTO.RoutineExecutionResultRes doSaveExecutionResult(
Long memberId,
RoutineExecutionRequestDTO.RoutineExecutionResultReq req
) {
try {
return transactionTemplate.execute(status -> {
Routine routine = routineRepository.findById(req.routineId())
.orElseThrow(() -> new BusinessException(ErrorStatus.ROUTINE_NOT_FOUND));

if (!routine.getRoutineGroup().isOwnedBy(memberId)) {
throw new BusinessException(ErrorStatus.ROUTINE_NOT_FOUND);
}

RoutineExecution routineExecution = findOrCreateExecution(
req.routineId(), req.executedDate(), routine, req
);
return RoutineExecutionConverter.toResponse(routineExecution);
});
} catch (DataIntegrityViolationException e) {
// λ™μ‹œ μš”μ²­μœΌλ‘œ λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ΄ λ¨Όμ € insert함 -> μƒˆ νŠΈλžœμž­μ…˜μ—μ„œ 재쑰회 ν›„ μ—…λ°μ΄νŠΈ
return transactionTemplate.execute(status -> {
RoutineExecution raceWinner = routineExecutionRepository
.findByRoutine_IdAndExecutedDate(req.routineId(), req.executedDate())
.orElseThrow(() -> e);
RoutineExecution updated = applyExecutionResult(raceWinner, req);
return RoutineExecutionConverter.toResponse(updated);
});
}
}

if (!routine.getRoutineGroup().isOwnedBy(memberId)) {
throw new BusinessException(ErrorStatus.ROUTINE_NOT_FOUND);
}
// try-catch 없이 "μ°Ύκ±°λ‚˜ μƒˆλ‘œ λ§Œλ“€κ±°λ‚˜"만 함 - μ˜ˆμ™ΈλŠ” κ·ΈλŒ€λ‘œ μœ„λ‘œ λ˜μ Έμ„œ νŠΈλžœμž­μ…˜μ΄ μžμ—°μŠ€λŸ½κ²Œ λ‘€λ°±
private RoutineExecution findOrCreateExecution(
Long routineId,
LocalDate executedDate,
Routine routine,
RoutineExecutionRequestDTO.RoutineExecutionResultReq req
) {
return routineExecutionRepository
.findByRoutine_IdAndExecutedDate(routineId, executedDate)
.map(existing -> applyExecutionResult(existing, req))
.orElseGet(() -> routineExecutionRepository.saveAndFlush(
RoutineExecutionConverter.toEntity(req, routine)
));
}

RoutineExecution routineExecution = RoutineExecutionConverter.toEntity(req, routine);
routineExecutionRepository.save(routineExecution);

return RoutineExecutionConverter.toResponse(routineExecution);
})
);
private RoutineExecution applyExecutionResult(
RoutineExecution existing,
RoutineExecutionRequestDTO.RoutineExecutionResultReq req
) {
if (Boolean.TRUE.equals(req.isCompleted())) {
existing.complete(req.durationSecond());
} else {
existing.fail(req.durationSecond());
}
if (req.memberInput() != null) {
existing.recordInput(req.memberInput());
}
if (req.aiResponse() != null) {
existing.recordAiResponse(req.aiResponse());
}
if (req.actualWakeTime() != null) {
existing.recordActualWakeTime(req.actualWakeTime());
}
return existing;
}


Expand All @@ -77,8 +135,7 @@ private RoutineExecutionResponseDTO.AiResponseRes doJudgeUserResponse(
Routine routine = routineRepository.findWithGroupById(req.routineId())
.orElseThrow(() -> new BusinessException(ErrorStatus.ROUTINE_NOT_FOUND));


if(!routine.getRoutineGroup().isOwnedBy(memberId)){
if (!routine.getRoutineGroup().isOwnedBy(memberId)) {
throw new BusinessException(ErrorStatus.ROUTINE_NOT_FOUND);
}

Expand All @@ -88,18 +145,66 @@ private RoutineExecutionResponseDTO.AiResponseRes doJudgeUserResponse(
throw new BusinessException(ErrorStatus.AI_JUDGE_FAILED);
}

if(dto.shouldProceed()){
RoutineExecution routineExecution = RoutineExecutionConverter.toEntity(req,routine,dto.aiResponse());
routineExecutionRepository.save(routineExecution);
if (dto.shouldProceed()) {
saveJudgeResultWithRetry(req, routine, dto.aiResponse());
}


return RoutineExecutionResponseDTO.AiResponseRes.builder()
.aiResponse(dto.aiResponse())
.shouldProceed(dto.shouldProceed())
.build();
}

private void saveJudgeResultWithRetry(
RoutineExecutionRequestDTO.AiResponseReq req,
Routine routine,
String aiResponse
) {
try {
transactionTemplate.execute(status -> {
findOrCreateJudgeResult(req.routineId(), req.executedDate(), routine, req, aiResponse);
return null;
});
} catch (DataIntegrityViolationException e) {
transactionTemplate.execute(status -> {
RoutineExecution raceWinner = routineExecutionRepository
.findByRoutine_IdAndExecutedDate(req.routineId(), req.executedDate())
.orElseThrow(() -> e);
applyJudgeResult(raceWinner, req, aiResponse);
return null;
});
}
}

private RoutineExecution findOrCreateJudgeResult(
Long routineId,
LocalDate executedDate,
Routine routine,
RoutineExecutionRequestDTO.AiResponseReq req,
String aiResponse
) {
return routineExecutionRepository
.findByRoutine_IdAndExecutedDate(routineId, executedDate)
.map(existing -> applyJudgeResult(existing, req, aiResponse))
.orElseGet(() -> routineExecutionRepository.saveAndFlush(
RoutineExecutionConverter.toEntity(req, routine, aiResponse)
));
}

private RoutineExecution applyJudgeResult(
RoutineExecution existing,
RoutineExecutionRequestDTO.AiResponseReq req,
String aiResponse
) {
existing.complete(req.durationSecond());
existing.recordAiResponse(aiResponse);
if (req.memberInput() != null) {
existing.recordInput(req.memberInput());
}
if (req.actualWakeTime() != null) {
existing.recordActualWakeTime(req.actualWakeTime());
}
return existing;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
DELETE re1 FROM routine_execution re1
INNER JOIN routine_execution re2
ON re1.routine_id = re2.routine_id
AND re1.executed_date = re2.executed_date
AND re1.id < re2.id;

ALTER TABLE routine_execution
ADD CONSTRAINT uk_routine_execution_routine_date
UNIQUE (routine_id, executed_date);
Loading