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
2 changes: 2 additions & 0 deletions src/main/java/org/runimo/runimo/common/GlobalConsts.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ public final class GlobalConsts {
"/v3/api-docs"
);

public static final String EMPTYFIELD = "EMPTY";

}
3 changes: 3 additions & 0 deletions src/main/java/org/runimo/runimo/item/domain/Egg.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
import lombok.Getter;
import lombok.NoArgsConstructor;

import static org.runimo.runimo.common.GlobalConsts.EMPTYFIELD;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Egg extends Item {
public static final Egg EMPTY = new Egg(EMPTYFIELD, EMPTYFIELD, EMPTYFIELD, EMPTYFIELD, null, 0L);
@Column(name = "egg_type")
@Enumerated(EnumType.STRING)
private EggType eggType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.runimo.runimo.common.response.SuccessResponse;
import org.runimo.runimo.records.controller.requests.RecordSaveRequest;
import org.runimo.runimo.records.controller.requests.RecordUpdateRequest;
import org.runimo.runimo.records.enums.RecordHttpResponse;
import org.runimo.runimo.records.service.usecases.RecordCreateUsecase;
import org.runimo.runimo.records.service.usecases.RecordQueryUsecase;
import org.runimo.runimo.records.service.usecases.RecordUpdateUsecase;
Expand Down Expand Up @@ -39,11 +41,13 @@ public class RecordController {
@ApiResponse(responseCode = "401", description = "인증 실패")
})
@PostMapping
public ResponseEntity<RecordSaveResponse> saveRecord(
@RequestBody RecordSaveRequest request
public ResponseEntity<SuccessResponse<RecordSaveResponse>> saveRecord(
@RequestBody RecordSaveRequest request,
@UserId Long userId
) {
RecordSaveResponse response = recordCreateUsecase.execute(RecordCreateCommand.from(request));
return ResponseEntity.created(URI.create("/api/v1/records/" + response.savedId())).body(response);
RecordSaveResponse response = recordCreateUsecase.execute(RecordCreateCommand.from(request, userId));
return ResponseEntity.created(URI.create("/api/v1/records/" + response.savedId()))
.body(SuccessResponse.of(RecordHttpResponse.RECORD_SAVED, response));
}

@Operation(summary = "기록 조회", description = "기록을 조회합니다.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@

@Schema(description = "사용자 달리기 기록 저장 요청 DTO")
public record RecordSaveRequest(
@Schema(description = "사용자 고유 식별자", example = "c7b3b3b3-7b3b-4b3b-8b3b-3b3b3b3b3b3b")
String userPublicId,
@Schema(description = "달리기 제목", example = "오늘의 달리기")
LocalDateTime startedAt,
@Schema(description = "달리기 시작 시각", example = "2021-10-10T10:10:10")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,17 @@ public class RunningRecord extends BaseEntity {
private Distance totalDistance;
@Embedded
private Pace averagePace;
@Column(name = "is_rewarded", nullable = false)
private Boolean isRewarded;

@Builder
public RunningRecord(Long userId, String title, LocalDateTime startedAt, LocalDateTime endAt, Distance totalDistance, Pace averagePace) {
public RunningRecord(Long userId, String title, LocalDateTime startedAt, LocalDateTime endAt, Distance totalDistance, Pace averagePace, Boolean isRewarded) {
this.userId = userId;
this.title = title;
this.recordPublicId = UUID.randomUUID().toString();
this.startedAt = startedAt;
this.endAt = endAt;
this.isRewarded = isRewarded;
this.totalDistance = totalDistance;
this.averagePace = averagePace;
}
Expand All @@ -59,8 +62,17 @@ public void update(RunningRecord updatedEntity) {
this.endAt = updatedEntity.getEndAt();
this.averagePace = updatedEntity.averagePace;
this.recordPublicId = updatedEntity.recordPublicId;
this.isRewarded = updatedEntity.isRewarded;
}

public void reward(Long editorId) {
validateEditor(editorId);
this.isRewarded = true;
}

public boolean isRecordAlreadyRewarded() {
return this.isRewarded;
}

private void validateEditor(Long editorId) {
if (editorId == null || !Objects.equals(this.userId, editorId)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.runimo.runimo.records.enums;
import org.runimo.runimo.exceptions.code.CustomResponseCode;
import org.springframework.http.HttpStatus;

public enum RecordHttpResponse implements CustomResponseCode {
RECORD_SAVED("RSH2001", "달리기 기록 저장 성공", "달리기 기록 저장 성공"),
;

private final String code;
private final String clientMessage;
private final String logMessage;

RecordHttpResponse(String code, String clientMessage, String logMessage) {
this.code = code;
this.clientMessage = clientMessage;
this.logMessage = logMessage;
}

@Override
public String getCode() {
return this.code;
}

@Override
public String getClientMessage() {
return this.clientMessage;
}

@Override
public String getLogMessage() {
return this.logMessage;
}

@Override
public HttpStatus getHttpStatusCode() {
return null;
}
}


Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
package org.runimo.runimo.records.repository;

import org.runimo.runimo.records.domain.RunningRecord;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.Optional;

@Repository
public interface RecordRepository extends JpaRepository<RunningRecord, Long> {
Optional<RunningRecord> findByRecordPublicId(String id);

@Query("SELECT r FROM RunningRecord r " +
"WHERE r.userId = :userId " +
"AND r.startedAt BETWEEN :startOfWeek AND :now")
Slice<RunningRecord> findFirstRunOfWeek(
@Param("userId") Long userId,
@Param("startOfWeek") LocalDateTime startOfWeek,
@Param("now") LocalDateTime now,
Pageable pageable
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class RecordCommandService {

@Transactional
public RecordSaveResponse saveRecord(Long userId, RecordCreateCommand command) {
RunningRecord runningRecord = mapToRunningRecord(userId, command);
RunningRecord runningRecord = mapToCreatedRunningRecord(userId, command);
recordRepository.save(runningRecord);
return new RecordSaveResponse(runningRecord.getId());
}
Expand All @@ -45,13 +45,14 @@ private RunningRecord mapToUpdateRecord(RecordUpdateCommand command) {
);
}

private RunningRecord mapToRunningRecord(Long id, RecordCreateCommand command) {
private RunningRecord mapToCreatedRunningRecord(Long id, RecordCreateCommand command) {
return RunningRecord.builder()
.userId(id)
.startedAt(command.startedAt())
.endAt(command.endAt())
.averagePace(command.averagePace())
.totalDistance(command.totalDistance())
.isRewarded(false)
.build();
}
}
11 changes: 11 additions & 0 deletions src/main/java/org/runimo/runimo/records/service/RecordFinder.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import lombok.RequiredArgsConstructor;
import org.runimo.runimo.records.domain.RunningRecord;
import org.runimo.runimo.records.repository.RecordRepository;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.Optional;

@Component
Expand All @@ -23,4 +26,12 @@ public Optional<RunningRecord> findById(Long id) {
public Optional<RunningRecord> findByPublicId(String id) {
return recordRepository.findByRecordPublicId(id);
}

@Transactional(readOnly = true)
public Optional<RunningRecord> findFirstRunOfCurrentWeek(Long userId) {
LocalDateTime now = LocalDateTime.now();
LocalDateTime startOfWeek = now.minusDays(now.getDayOfWeek().getValue() - 1L).withHour(0).withMinute(0).withSecond(0);
PageRequest pageRequest = PageRequest.of(0, 1, Sort.by("startedAt").ascending());
return recordRepository.findFirstRunOfWeek(userId, startOfWeek, now, pageRequest).stream().findFirst();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class RecordCreateUsecaseImpl implements RecordCreateUsecase {
@Override
@Transactional
public RecordSaveResponse execute(RecordCreateCommand command) {
User user = userFinder.findUserByPublicId(command.userPublicId())
User user = userFinder.findUserById(command.userId())
.orElseThrow(NoSuchElementException::new);
userStatService.updateUserStats(user, command);
return commandService.saveRecord(user.getId(), command);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@
import java.time.temporal.ChronoUnit;

public record RecordCreateCommand(
String userPublicId,
Long userId,
LocalDateTime startedAt,
LocalDateTime endAt,
Pace averagePace,
Distance totalDistance
) {

public static RecordCreateCommand from(RecordSaveRequest request) {
public static RecordCreateCommand from(final RecordSaveRequest request, final Long userId) {
return new RecordCreateCommand(
request.userPublicId(),
userId,
request.startedAt(),
request.endAt(),
new Pace(request.averagePaceInMilliSeconds()),
Expand Down
34 changes: 34 additions & 0 deletions src/main/java/org/runimo/runimo/rewards/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 보상 지급관련 패키지

## 요구사항

### 1. 주간 첫번째 달리기를 마치면 알을 지급한다.

- 사용자는 주마다 첫번째 달리기를 마치면 `알`을 보상으로 받는다.
- ex) 1주일동안 3번 달렸다면, 1번째 달린 날에 `알`을 받는다.


- `알`은 사용자의 누적 달리기 거리에 따라 출현 확률이 결정된다.
- ex) 사용자가 30km를 누적으로 달렸다면 2가지 알이 해금되었고, 1/2의 확률로 각각의 알이 출현한다.

#### 제한 사항:
- 주간은 월요일 ~ 일요일 단위로 계산한다.
- 주간 첫번째 달리기는 월요일 00:00:00 ~ 일요일 23:59:59 사이에 달려야 한다.
- 사용자는 주간 첫번째 달리기를 마치면 `알`을 받는다.
- 이미 달린 경우 알을 받을 수 없다.
- **알 지급에 대한 기록을 남겨야한다.**


### 2. 매 달리기마다 애정을 지급한다.

- 사용자는 매 달리기마다 `애정`을 보상으로 받는다.


- `애정`은 1km에 1개씩 지급된다.


- 달린거리를 `내림` 하여 `애정`을 지급한다.
- ex) 3.5km를 달렸다면 3개의 `애정`을 받는다.
- ex) 3.1km를 달렸다면 3개의 `애정`을 받는다.
- ex) 2.9km를 달렸다면 2개의 `애정`을 받는다.

41 changes: 36 additions & 5 deletions src/main/java/org/runimo/runimo/rewards/service/RewardService.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
package org.runimo.runimo.rewards.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.runimo.runimo.item.domain.Egg;
import org.runimo.runimo.records.domain.RunningRecord;
import org.runimo.runimo.records.service.RecordFinder;
import org.runimo.runimo.rewards.service.eggs.EggGrantService;
import org.runimo.runimo.rewards.service.dtos.RewardClaimCommand;
import org.runimo.runimo.rewards.service.dtos.RewardResponse;
import org.runimo.runimo.user.domain.User;
import org.runimo.runimo.user.service.UserFinder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.NoSuchElementException;
import java.util.Optional;

@Slf4j
@Service
@RequiredArgsConstructor
public class RewardService {
Expand All @@ -20,14 +25,40 @@ public class RewardService {
private final UserFinder userFinder;
private final EggGrantService eggGrantService;

@Transactional
public RewardResponse claimReward(RewardClaimCommand command) {
User user = userFinder.findUserById(command.userId())
RunningRecord runningRecord = recordFinder.findById(command.recordId())
.orElseThrow(NoSuchElementException::new);
recordFinder.findById(command.recordId())
.orElseThrow(NoSuchElementException::new);
// grant egg
Egg grantedEgg = eggGrantService.grantRandomEggToUser(user);
validateRecord(runningRecord);
Egg grantedEgg = rewardEgg(command);
runningRecord.reward(command.userId());
//TODO: 애정 보상
return new RewardResponse(grantedEgg.getItemCode(), grantedEgg.getEggType());
}

private Egg rewardEgg(RewardClaimCommand command) {
User user = userFinder.findUserById(command.userId())
.orElseThrow(NoSuchElementException::new);
if(validateRecordIsFirstRecordOfWeek(command)) {
return eggGrantService.grantRandomEggToUser(user);
}
return Egg.EMPTY;
}

private void validateRecord(RunningRecord runningRecord) {
if(runningRecord.isRecordAlreadyRewarded()) throw new IllegalStateException("이미 보상이 지급되었습니다.");
}

private boolean validateRecordIsFirstRecordOfWeek(RewardClaimCommand command) {
Optional<RunningRecord> firstRecordOfWeek = recordFinder.findFirstRunOfCurrentWeek(command.userId());
if(firstRecordOfWeek.isEmpty()) {
log.info("유저 {}의 첫번째 달리기 기록이 없습니다.", command.userId());
return false;
}
if(!command.recordId().equals(firstRecordOfWeek.get().getId())){
log.info("유저 {}의 첫번째 달리기 기록이 아닙니다.", command.userId());
return false;
}
return true;
}
}
1 change: 1 addition & 0 deletions src/main/resources/sql/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ CREATE TABLE `running_records`
`end_at` timestamp,
`total_distance` integer,
`pace_in_milli_seconds` integer,
`is_rewarded` boolean,
`created_at` timestamp,
`updated_at` timestamp,
`deleted_at` TIMESTAMP
Expand Down
3 changes: 2 additions & 1 deletion src/test/java/org/runimo/runimo/CleanUpUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ public class CleanUpUtil {
private static final String[] USER_TABLES = {
"user_item",
"oauth_accounts",
"users"
"users",
"running_records",
};

@Autowired
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/org/runimo/runimo/rewards/RewardTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ void tearDown() {
@Test
void 보상_요청_테스트() {
RecordCreateCommand recordCreateCommand = new RecordCreateCommand(
savedUser.getPublicId(),
savedUser.getId(),
LocalDateTime.now(),
LocalDateTime.now().plusHours(1),
new Pace(1909L),
Expand Down
Loading