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
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.runnect.server.common.advice;

import io.sentry.Sentry;
import java.io.IOException;
import java.util.Objects;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
Expand All @@ -10,6 +9,7 @@
import org.runnect.server.common.dto.ApiResponseDto;
import org.runnect.server.common.exception.BasicException;
import org.runnect.server.config.slack.SlackApi;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
Expand All @@ -21,6 +21,7 @@
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
@Component
@RequiredArgsConstructor
Expand Down Expand Up @@ -71,9 +72,17 @@ protected ApiResponseDto handleMissingRequestParameterException(final MissingSer
*/
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
protected ApiResponseDto<Object> handleException(final Exception error, final HttpServletRequest request) throws IOException {
slackApi.sendAlert(error, request);
Sentry.captureException(error);
protected ApiResponseDto<Object> handleException(final Exception error, final HttpServletRequest request) {
try {
slackApi.sendAlert(error, request);
} catch (Exception e) {
log.error("Slack 알림 전송 실패", e);
}
try {
Sentry.captureException(error);
} catch (Exception e) {
log.error("Sentry 전송 실패", e);
}
return ApiResponseDto.error(ErrorStatus.INTERNAL_SERVER_ERROR);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ public enum ErrorStatus {
NOT_FOUND_SCRAP_EXCEPTION(HttpStatus.BAD_REQUEST, "스크랩한 코스가 없습니다."),
NOT_FOUND_IMAGE_EXCEPTION(HttpStatus.BAD_REQUEST, "잘못된 이미지 파일입니다"),
NOT_FOUND_PUBLICCOURSE_EXCEPTION(HttpStatus.BAD_REQUEST, "존재하지 않는 public course id입니다."),
INVALID_HEALTH_DATA_EXCEPTION(HttpStatus.BAD_REQUEST, "유효하지 않은 건강 데이터입니다"),
INVALID_DATE_RANGE_EXCEPTION(HttpStatus.BAD_REQUEST, "유효하지 않은 날짜 범위입니다"),
EXCEED_HEART_RATE_SAMPLES_EXCEPTION(HttpStatus.BAD_REQUEST, "심박수 샘플은 최대 5000건까지 허용됩니다"),

/**
* 401 UNAUTHORIZED
Expand All @@ -51,6 +54,7 @@ public enum ErrorStatus {
*/
PERMISSION_DENIED_PUBLIC_COURSE_DELETE_EXCEPTION(HttpStatus.FORBIDDEN, "퍼블릭 코스를 삭제할 권한이 존재하지 않습니다."),
PERMISSION_DENIED_RECORD_DELETE_EXCEPTION(HttpStatus.FORBIDDEN, "기록을 삭제할 권한이 존재하지 않습니다."),
PERMISSION_DENIED_HEALTH_DATA_EXCEPTION(HttpStatus.FORBIDDEN, "건강 데이터에 대한 접근 권한이 없습니다"),

/**
* 404 NOT FOUND
Expand All @@ -63,6 +67,7 @@ public enum ErrorStatus {
ALREADY_EXIST_USER_EXCEPTION(HttpStatus.CONFLICT, "이미 존재하는 유저입니다"),
ALREADY_EXIST_NICKNAME_EXCEPTION(HttpStatus.CONFLICT, "중복된 닉네임입니다."),
ALREADY_UPLOAD_COURSE_EXCEPTION(HttpStatus.CONFLICT, "이미 업로드된 코스입니다."),
ALREADY_EXIST_HEALTH_DATA_EXCEPTION(HttpStatus.CONFLICT, "이미 건강 데이터가 등록된 기록입니다"),

/**
* 500 INTERNAL SERVER ERROR
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ public enum SuccessStatus {

SEARCH_PUBLIC_COURSE_SUCCESS(HttpStatus.OK,"업로드된 코스 검색 성공"),

GET_HEALTH_DATA_SUCCESS(HttpStatus.OK, "건강 데이터 조회 성공"),
GET_HEALTH_SUMMARY_SUCCESS(HttpStatus.OK, "건강 통계 조회 성공"),


UPDATE_RECORD_SUCCESS(HttpStatus.OK, "활동 기록 수정 성공"),
UPDATE_USER_NICKNAME_SUCCESS(HttpStatus.OK, "닉네임 변경에 성공했습니다."),
Expand All @@ -41,6 +44,7 @@ public enum SuccessStatus {
DELETE_PUBLIC_COURSE_SUCCESS(HttpStatus.OK, "퍼블릭 코스 삭제에 성공했습니다."),
DELETE_RECORD_SUCCESS(HttpStatus.OK, "기록 삭제에 성공했습니다."),
DELETE_COURSES_SUCCESS(HttpStatus.OK, "코스 삭제 성공"),
DELETE_HEALTH_DATA_SUCCESS(HttpStatus.OK, "건강 데이터 삭제 성공"),


/**
Expand All @@ -52,6 +56,7 @@ public enum SuccessStatus {
CREATE_PUBLIC_COURSE_SUCCESS(HttpStatus.CREATED, "코드 업로드에 성공했습니다."),
CREATE_SCRAP_SUCCESS(HttpStatus.CREATED, "코스 스크랩 성공"),
NEW_TOKEN_SUCCESS(HttpStatus.CREATED, "토큰 재발급에 성공했습니다."),
CREATE_HEALTH_DATA_SUCCESS(HttpStatus.CREATED, "건강 데이터 저장 성공"),
;

private final HttpStatus httpStatus;
Expand Down
9 changes: 5 additions & 4 deletions src/main/java/org/runnect/server/config/slack/SlackApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ public class SlackApi {
private final static String NEW_LINE = "\n";
private final static String DOUBLE_NEW_LINE = "\n\n";

private StringBuilder sb = new StringBuilder();

public void sendAlert(Exception error, HttpServletRequest request) throws IOException {

List<LayoutBlock> layoutBlocks = generateLayoutBlock(error, request);
Expand All @@ -53,15 +51,15 @@ private List<LayoutBlock> generateLayoutBlock(Exception error, HttpServletReques
}

private String generateErrorMessage(Exception error) {
sb.setLength(0);
StringBuilder sb = new StringBuilder();
sb.append("*[🔥 Exception]*" + NEW_LINE + error.toString() + DOUBLE_NEW_LINE);
sb.append("*[📩 From]*" + NEW_LINE + readRootStackTrace(error) + DOUBLE_NEW_LINE);

return sb.toString();
}

private String generateErrorPointMessage(HttpServletRequest request) {
sb.setLength(0);
StringBuilder sb = new StringBuilder();
sb.append("*[🧾세부정보]*" + NEW_LINE);
sb.append("Request URL : " + request.getRequestURL().toString() + NEW_LINE);
sb.append("Request Method : " + request.getMethod() + NEW_LINE);
Expand All @@ -71,6 +69,9 @@ private String generateErrorPointMessage(HttpServletRequest request) {
}

private String readRootStackTrace(Exception error) {
if (error.getStackTrace() == null || error.getStackTrace().length == 0) {
return "Unknown";
}
return error.getStackTrace()[0].toString();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package org.runnect.server.health.controller;

import javax.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.runnect.server.common.constant.SuccessStatus;
import org.runnect.server.common.dto.ApiResponseDto;
import org.runnect.server.common.resolver.userId.UserId;
import org.runnect.server.health.dto.request.HealthDataRequestDto;
import org.runnect.server.health.dto.response.CreateHealthDataResponseDto;
import org.runnect.server.health.dto.response.GetHealthDataResponseDto;
import org.runnect.server.health.dto.response.GetHealthSummaryResponseDto;
import org.runnect.server.health.service.HealthService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class HealthController {

private final HealthService healthService;

@PostMapping("record/{recordId}/health")
@ResponseStatus(HttpStatus.CREATED)
public ApiResponseDto<CreateHealthDataResponseDto> createHealthData(
@UserId Long userId,
@PathVariable(name = "recordId") Long recordId,
@RequestBody @Valid final HealthDataRequestDto request) {
return ApiResponseDto.success(SuccessStatus.CREATE_HEALTH_DATA_SUCCESS,
healthService.createHealthData(userId, recordId, request));
}

@GetMapping("record/{recordId}/health")
@ResponseStatus(HttpStatus.OK)
public ApiResponseDto<GetHealthDataResponseDto> getHealthData(
@UserId Long userId,
@PathVariable(name = "recordId") Long recordId) {
return ApiResponseDto.success(SuccessStatus.GET_HEALTH_DATA_SUCCESS,
healthService.getHealthData(userId, recordId));
}

@GetMapping("health/summary")
@ResponseStatus(HttpStatus.OK)
public ApiResponseDto<GetHealthSummaryResponseDto> getHealthSummary(
@UserId Long userId,
@RequestParam(name = "startDate") String startDate,
@RequestParam(name = "endDate") String endDate) {
return ApiResponseDto.success(SuccessStatus.GET_HEALTH_SUMMARY_SUCCESS,
healthService.getHealthSummary(userId, startDate, endDate));
}

@DeleteMapping("record/{recordId}/health")
@ResponseStatus(HttpStatus.OK)
public ApiResponseDto deleteHealthData(
@UserId Long userId,
@PathVariable(name = "recordId") Long recordId) {
healthService.deleteHealthData(userId, recordId);
return ApiResponseDto.success(SuccessStatus.DELETE_HEALTH_DATA_SUCCESS);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.runnect.server.health.dto.request;

import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
public class HealthDataRequestDto {
@NotNull
private Double avgHeartRate;

private Double maxHeartRate;

private Double minHeartRate;

@NotNull
private Double calories;

@NotNull
private Integer zone1Seconds;

@NotNull
private Integer zone2Seconds;

@NotNull
private Integer zone3Seconds;

@NotNull
private Integer zone4Seconds;

@NotNull
private Integer zone5Seconds;

private Double maxHeartRateConfig;

@Valid
private List<HeartRateSampleRequestDto> heartRateSamples;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.runnect.server.health.dto.request;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
public class HeartRateSampleRequestDto {
@NotNull
private Double heartRate;

@NotNull
private Integer elapsedSeconds;

@NotNull
@Min(1) @Max(5)
private Integer zone;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.runnect.server.health.dto.response;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class CreateHealthDataResponseDto {
private Long healthDataId;

public static CreateHealthDataResponseDto of(Long healthDataId) {
return new CreateHealthDataResponseDto(healthDataId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.runnect.server.health.dto.response;

import java.util.List;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class GetHealthDataResponseDto {
private HealthDataDetailResponse healthData;

public static GetHealthDataResponseDto of(HealthDataDetailResponse healthData) {
return new GetHealthDataResponseDto(healthData);
}

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class HealthDataDetailResponse {
private Long id;
private Long recordId;
private Double avgHeartRate;
private Double maxHeartRate;
private Double minHeartRate;
private Double calories;
private ZoneResponse zones;
private Double maxHeartRateConfig;
private List<HeartRateSampleResponse> heartRateSamples;

public static HealthDataDetailResponse of(Long id, Long recordId, Double avgHeartRate,
Double maxHeartRate, Double minHeartRate, Double calories,
ZoneResponse zones, Double maxHeartRateConfig,
List<HeartRateSampleResponse> heartRateSamples) {
return new HealthDataDetailResponse(id, recordId, avgHeartRate, maxHeartRate,
minHeartRate, calories, zones, maxHeartRateConfig, heartRateSamples);
}
}

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class ZoneResponse {
private Integer zone1Seconds;
private Integer zone2Seconds;
private Integer zone3Seconds;
private Integer zone4Seconds;
private Integer zone5Seconds;

public static ZoneResponse of(Integer zone1Seconds, Integer zone2Seconds,
Integer zone3Seconds, Integer zone4Seconds, Integer zone5Seconds) {
return new ZoneResponse(zone1Seconds, zone2Seconds, zone3Seconds, zone4Seconds, zone5Seconds);
}
}

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class HeartRateSampleResponse {
private Double heartRate;
private Integer elapsedSeconds;
private Integer zone;

public static HeartRateSampleResponse of(Double heartRate, Integer elapsedSeconds, Integer zone) {
return new HeartRateSampleResponse(heartRate, elapsedSeconds, zone);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.runnect.server.health.dto.response;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class GetHealthSummaryResponseDto {
private HealthSummaryResponse summary;

public static GetHealthSummaryResponseDto of(HealthSummaryResponse summary) {
return new GetHealthSummaryResponseDto(summary);
}

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class HealthSummaryResponse {
private Long totalRecords;
private Long recordsWithHealth;
private Double avgHeartRate;
private Double avgCalories;
private Double totalCalories;
private GetHealthDataResponseDto.ZoneResponse zoneDistribution;

public static HealthSummaryResponse of(Long totalRecords, Long recordsWithHealth,
Double avgHeartRate, Double avgCalories, Double totalCalories,
GetHealthDataResponseDto.ZoneResponse zoneDistribution) {
return new HealthSummaryResponse(totalRecords, recordsWithHealth, avgHeartRate,
avgCalories, totalCalories, zoneDistribution);
}
}
}
Loading
Loading