Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: RunningRecord controller record 추가 api 추가 #52

Merged
merged 7 commits into from
Aug 8, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.dnd.runus.application.running;

import com.dnd.runus.domain.member.Member;
import com.dnd.runus.domain.member.MemberRepository;
import com.dnd.runus.domain.running.RunningRecord;
import com.dnd.runus.domain.running.RunningRecordRepository;
import com.dnd.runus.global.exception.BusinessException;
import com.dnd.runus.global.exception.NotFoundException;
import com.dnd.runus.global.exception.type.ErrorType;
import com.dnd.runus.presentation.v1.running.dto.request.RunningRecordRequest;
import com.dnd.runus.presentation.v1.running.dto.response.RunningRecordReportResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.ZoneOffset;

@Service
public class RunningRecordService {
private final RunningRecordRepository runningRecordRepository;
private final MemberRepository memberRepository;
private final ZoneOffset defaultZoneOffset;

public RunningRecordService(
RunningRecordRepository runningRecordRepository,
MemberRepository memberRepository,
@Value("${app.default-zone-offset}") ZoneOffset defaultZoneOffset) {
this.runningRecordRepository = runningRecordRepository;
this.memberRepository = memberRepository;
this.defaultZoneOffset = defaultZoneOffset;
}

@Transactional
public RunningRecordReportResponse addRunningRecord(long memberId, RunningRecordRequest request) {
if (request.startAt().isAfter(request.endAt())) {
throw new BusinessException(ErrorType.START_AFTER_END, request.startAt() + ", " + request.endAt());
}
if (request.runningData().route().size() < 2) {
throw new BusinessException(
ErrorType.ROUTE_MUST_HAVE_AT_LEAST_TWO_COORDINATES,
request.runningData().route().toString());
}
Jaewon-pro marked this conversation as resolved.
Show resolved Hide resolved
// FIXME: badge에 left join해서 같이 조회하기
Member member =
memberRepository.findById(memberId).orElseThrow(() -> new NotFoundException(Member.class, memberId));
// TODO: 챌린지 기능 추가 후 수정
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 스크럼때, 여기 이슈에서 논의해보면 좋을 것 같아요!


RunningRecord record = runningRecordRepository.save(RunningRecord.builder()
.member(member)
.startAt(request.startAt().atOffset(defaultZoneOffset))
.endAt(request.endAt().atOffset(defaultZoneOffset))
.route(request.runningData().route())
.emoji(request.emoji())
.location(request.runningData().location())
.distanceMeter(request.runningData().distanceMeter())
.duration(request.runningData().runningTime())
.calorie(request.runningData().calorie())
.averagePace(request.runningData().averagePace())
.build());

return RunningRecordReportResponse.from(record);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package com.dnd.runus.domain.running;

import java.util.Optional;

public interface RunningRecordRepository {
Optional<RunningRecord> findById(long runningRecordId);

RunningRecord save(RunningRecord runningRecord);

void deleteByMemberId(long memberId);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.dnd.runus.global.constant;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.RequiredArgsConstructor;

import java.util.Arrays;
Expand All @@ -11,7 +12,7 @@
public enum RunningEmoji {
VERY_BAD("very-bad"),
BAD("bad"),
NORMAL("normal"),
SOSO("soso"),
GOOD("good"),
VERY_GOOD("very-good"),
;
Expand All @@ -24,4 +25,9 @@ public static RunningEmoji from(String code) {
.findAny()
.orElseThrow(() -> new IllegalArgumentException("Unknown RunningEmoji code: " + code));
}

@JsonValue
public String getCode() {
return code;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ public enum ErrorType {
// DatabaseErrorType
ENTITY_NOT_FOUND(NOT_FOUND, "DB_001", "해당 엔티티를 찾을 수 없습니다"),
VIOLATION_OCCURRED(NOT_ACCEPTABLE, "DB_002", "저장할 수 없는 값입니다"),

// TimeErrorType
START_AFTER_END(BAD_REQUEST, "TIME_001", "시작 시간이 종료 시간보다 빨라야 합니다"),

// RunningErrorType
ROUTE_MUST_HAVE_AT_LEAST_TWO_COORDINATES(BAD_REQUEST, "RUNNING_001", "경로는 최소 2개의 좌표를 가져야 합니다"),
;
private final HttpStatus httpStatus;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
package com.dnd.runus.infrastructure.persistence.domain.running;

import com.dnd.runus.domain.running.RunningRecord;
import com.dnd.runus.domain.running.RunningRecordRepository;
import com.dnd.runus.infrastructure.persistence.jpa.running.JpaRunningRecordRepository;
import com.dnd.runus.infrastructure.persistence.jpa.running.entity.RunningRecordEntity;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
@RequiredArgsConstructor
public class RunningRecordRepositoryImpl implements RunningRecordRepository {

private final JpaRunningRecordRepository jpaRunningRecordRepository;

@Override
public Optional<RunningRecord> findById(long runningRecordId) {
return jpaRunningRecordRepository.findById(runningRecordId).map(RunningRecordEntity::toDomain);
}

@Override
public RunningRecord save(RunningRecord runningRecord) {
return jpaRunningRecordRepository
.save(RunningRecordEntity.from(runningRecord))
.toDomain();
}

@Override
public void deleteByMemberId(long memberId) {
jpaRunningRecordRepository.deleteByMemberId(memberId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.dnd.runus.presentation.v1.running;

import com.dnd.runus.application.running.RunningRecordService;
import com.dnd.runus.global.exception.type.ApiErrorType;
import com.dnd.runus.global.exception.type.ErrorType;
import com.dnd.runus.presentation.annotation.MemberId;
import com.dnd.runus.presentation.v1.running.dto.request.RunningRecordRequest;
import com.dnd.runus.presentation.v1.running.dto.response.RunningRecordReportResponse;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/running-records")
public class RunningRecordController {
private final RunningRecordService runningRecordService;

@Operation(
summary = "러닝 기록 추가 API. RunningRecordRequest, RunningRecordReportResponse 스키마를 참고해 주세요.",
description = "러닝 기록을 추가합니다. RunningRecordRequest, RunningRecordSavingResponse 스키마를 참고해 주세요.<br>"
+ "러닝 기록은 시작 시간, 종료 시간, 이모지, 챌린지 ID, 러닝 데이터로 구성됩니다. <br> "
+ "러닝 데이터는 route(코스), 위치, 거리, 시간, 칼로리, 평균 페이스로 구성됩니다. <br> "
+ "route는 최소 2개의 좌표를 가져야 합니다. <br> "
+ "러닝 기록 추가에 성공하면 러닝 기록 ID, 기록 정보와 사용자 닉네임, 프로필 url을 반환합니다. <br>")
@ApiErrorType({ErrorType.START_AFTER_END, ErrorType.ROUTE_MUST_HAVE_AT_LEAST_TWO_COORDINATES})
@PostMapping
@ResponseStatus(HttpStatus.OK)
public RunningRecordReportResponse addRunningRecord(
@MemberId long memberId, @Valid @RequestBody RunningRecordRequest request) {
return runningRecordService.addRunningRecord(memberId, request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.dnd.runus.presentation.v1.running.dto;

public record ChallengeDto(
long challengeId
// TODO: 챌린지 기능 확정후, 필요한 필드 추가
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.dnd.runus.presentation.v1.running.dto;

import com.dnd.runus.domain.common.Coordinate;
import com.dnd.runus.domain.common.Pace;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

import java.time.Duration;
import java.util.List;

public record RunningRecordMetricsDto(
@Schema(description = "평균 페이스", example = "5'30''")
Pace averagePace,
@NotBlank
@Schema(description = "시작 위치", example = "서울시,연남동")
String location,
@Schema(description = "멈춘 시간을 제외한 실제로 달린 시간", example = "123:45:56", format = "HH:mm:ss")
Duration runningTime,
int distanceMeter,
double calorie,
@Size(min = 2, message = "최소 2개의 좌표가 필요합니다.")
List<Coordinate> route
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.dnd.runus.presentation.v1.running.dto.request;

import com.dnd.runus.global.constant.RunningEmoji;
import com.dnd.runus.presentation.v1.running.dto.RunningRecordMetricsDto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

import java.time.LocalDateTime;

public record RunningRecordRequest(
@NotNull
LocalDateTime startAt,
@NotNull
LocalDateTime endAt,
@NotNull
RunningEmoji emoji,
@Schema(description = "챌린지 ID, 챌린지를 하지 않은 경우 null이나 필드 없이 보내주세요", example = "1")
Long challengeId,
@NotNull
RunningRecordMetricsDto runningData
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.dnd.runus.presentation.v1.running.dto.response;

import com.dnd.runus.domain.running.RunningRecord;
import com.dnd.runus.global.constant.RunningEmoji;
import com.dnd.runus.presentation.v1.running.dto.ChallengeDto;
import com.dnd.runus.presentation.v1.running.dto.RunningRecordMetricsDto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

import java.time.LocalDateTime;

public record RunningRecordReportResponse(
long runningRecordId,
LocalDateTime startAt,
LocalDateTime endAt,
RunningEmoji emoji,
String nickname,
@Schema(description = "프로필 이미지 URL")
String profileUrl,
@Schema(description = "챌린지 정보")
ChallengeDto challenge,
@NotNull
RunningRecordMetricsDto runningData
) {
public static RunningRecordReportResponse from(RunningRecord runningRecord) {
return new RunningRecordReportResponse(
runningRecord.runningId(),
runningRecord.startAt().toLocalDateTime(),
runningRecord.endAt().toLocalDateTime(),
runningRecord.emoji(),
runningRecord.member().nickname(),
runningRecord.member().mainBadge() == null ? null : runningRecord.member().mainBadge().imageUrl(),
new ChallengeDto(-1), // TODO: 챌린지 기능 추가 후 수정
new RunningRecordMetricsDto(
runningRecord.averagePace(),
runningRecord.location(),
runningRecord.duration(),
runningRecord.distanceMeter(),
runningRecord.calorie(),
runningRecord.route()
));
}
}
1 change: 1 addition & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ app:
access:
expiration: ${ACCESS_TOKEN_EXPIRATION}
secret-key: ${ACCESS_TOKEN_SECRET_KEY}
default-zone-offset: +09:00

oauth:
apple:
Expand Down
Loading