Skip to content

Commit b0e895e

Browse files
authored
Merge pull request #182 from Runnect/dev
[Feat] - μ• ν”Œμ›ŒμΉ˜ 건강 데이터 연동 + 배포 μ•ˆμ •μ„± κ°•ν™” (v3)
2 parents ca861a8 + 503ef8a commit b0e895e

18 files changed

Lines changed: 772 additions & 17 deletions

β€Žsrc/main/java/org/runnect/server/common/advice/ControllerExceptionAdvice.javaβ€Ž

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package org.runnect.server.common.advice;
22

33
import io.sentry.Sentry;
4-
import java.io.IOException;
54
import java.util.Objects;
65
import javax.servlet.http.HttpServletRequest;
76
import javax.validation.ConstraintViolationException;
@@ -10,6 +9,7 @@
109
import org.runnect.server.common.dto.ApiResponseDto;
1110
import org.runnect.server.common.exception.BasicException;
1211
import org.runnect.server.config.slack.SlackApi;
12+
import lombok.extern.slf4j.Slf4j;
1313
import org.springframework.http.HttpStatus;
1414
import org.springframework.http.ResponseEntity;
1515
import org.springframework.stereotype.Component;
@@ -21,6 +21,7 @@
2121
import org.springframework.web.bind.annotation.ResponseStatus;
2222
import org.springframework.web.bind.annotation.RestControllerAdvice;
2323

24+
@Slf4j
2425
@RestControllerAdvice
2526
@Component
2627
@RequiredArgsConstructor
@@ -71,9 +72,17 @@ protected ApiResponseDto handleMissingRequestParameterException(final MissingSer
7172
*/
7273
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
7374
@ExceptionHandler(Exception.class)
74-
protected ApiResponseDto<Object> handleException(final Exception error, final HttpServletRequest request) throws IOException {
75-
slackApi.sendAlert(error, request);
76-
Sentry.captureException(error);
75+
protected ApiResponseDto<Object> handleException(final Exception error, final HttpServletRequest request) {
76+
try {
77+
slackApi.sendAlert(error, request);
78+
} catch (Exception e) {
79+
log.error("Slack μ•Œλ¦Ό 전솑 μ‹€νŒ¨", e);
80+
}
81+
try {
82+
Sentry.captureException(error);
83+
} catch (Exception e) {
84+
log.error("Sentry 전솑 μ‹€νŒ¨", e);
85+
}
7786
return ApiResponseDto.error(ErrorStatus.INTERNAL_SERVER_ERROR);
7887
}
7988

@@ -86,5 +95,3 @@ protected ResponseEntity<ApiResponseDto> handleBasicException(BasicException e)
8695
.body(ApiResponseDto.error(e.getErrorStatus(), e.getMessage()));
8796
}
8897
}
89-
90-

β€Žsrc/main/java/org/runnect/server/common/constant/ErrorStatus.javaβ€Ž

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ public enum ErrorStatus {
3131
NOT_FOUND_SCRAP_EXCEPTION(HttpStatus.BAD_REQUEST, "μŠ€ν¬λž©ν•œ μ½”μŠ€κ°€ μ—†μŠ΅λ‹ˆλ‹€."),
3232
NOT_FOUND_IMAGE_EXCEPTION(HttpStatus.BAD_REQUEST, "잘λͺ»λœ 이미지 νŒŒμΌμž…λ‹ˆλ‹€"),
3333
NOT_FOUND_PUBLICCOURSE_EXCEPTION(HttpStatus.BAD_REQUEST, "μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” public course idμž…λ‹ˆλ‹€."),
34+
INVALID_HEALTH_DATA_EXCEPTION(HttpStatus.BAD_REQUEST, "μœ νš¨ν•˜μ§€ μ•Šμ€ 건강 λ°μ΄ν„°μž…λ‹ˆλ‹€"),
35+
INVALID_DATE_RANGE_EXCEPTION(HttpStatus.BAD_REQUEST, "μœ νš¨ν•˜μ§€ μ•Šμ€ λ‚ μ§œ λ²”μœ„μž…λ‹ˆλ‹€"),
36+
EXCEED_HEART_RATE_SAMPLES_EXCEPTION(HttpStatus.BAD_REQUEST, "μ‹¬λ°•μˆ˜ μƒ˜ν”Œμ€ μ΅œλŒ€ 5000κ±΄κΉŒμ§€ ν—ˆμš©λ©λ‹ˆλ‹€"),
3437

3538
/**
3639
* 401 UNAUTHORIZED
@@ -51,6 +54,7 @@ public enum ErrorStatus {
5154
*/
5255
PERMISSION_DENIED_PUBLIC_COURSE_DELETE_EXCEPTION(HttpStatus.FORBIDDEN, "퍼블릭 μ½”μŠ€λ₯Ό μ‚­μ œν•  κΆŒν•œμ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."),
5356
PERMISSION_DENIED_RECORD_DELETE_EXCEPTION(HttpStatus.FORBIDDEN, "기둝을 μ‚­μ œν•  κΆŒν•œμ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."),
57+
PERMISSION_DENIED_HEALTH_DATA_EXCEPTION(HttpStatus.FORBIDDEN, "건강 데이터에 λŒ€ν•œ μ ‘κ·Ό κΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€"),
5458

5559
/**
5660
* 404 NOT FOUND
@@ -63,6 +67,7 @@ public enum ErrorStatus {
6367
ALREADY_EXIST_USER_EXCEPTION(HttpStatus.CONFLICT, "이미 μ‘΄μž¬ν•˜λŠ” μœ μ €μž…λ‹ˆλ‹€"),
6468
ALREADY_EXIST_NICKNAME_EXCEPTION(HttpStatus.CONFLICT, "μ€‘λ³΅λœ λ‹‰λ„€μž„μž…λ‹ˆλ‹€."),
6569
ALREADY_UPLOAD_COURSE_EXCEPTION(HttpStatus.CONFLICT, "이미 μ—…λ‘œλ“œλœ μ½”μŠ€μž…λ‹ˆλ‹€."),
70+
ALREADY_EXIST_HEALTH_DATA_EXCEPTION(HttpStatus.CONFLICT, "이미 건강 데이터가 λ“±λ‘λœ κΈ°λ‘μž…λ‹ˆλ‹€"),
6671

6772
/**
6873
* 500 INTERNAL SERVER ERROR

β€Žsrc/main/java/org/runnect/server/common/constant/SuccessStatus.javaβ€Ž

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ public enum SuccessStatus {
2929

3030
SEARCH_PUBLIC_COURSE_SUCCESS(HttpStatus.OK,"μ—…λ‘œλ“œλœ μ½”μŠ€ 검색 성곡"),
3131

32+
GET_HEALTH_DATA_SUCCESS(HttpStatus.OK, "건강 데이터 쑰회 성곡"),
33+
GET_HEALTH_SUMMARY_SUCCESS(HttpStatus.OK, "건강 톡계 쑰회 성곡"),
34+
3235

3336
UPDATE_RECORD_SUCCESS(HttpStatus.OK, "ν™œλ™ 기둝 μˆ˜μ • 성곡"),
3437
UPDATE_USER_NICKNAME_SUCCESS(HttpStatus.OK, "λ‹‰λ„€μž„ 변경에 μ„±κ³΅ν–ˆμŠ΅λ‹ˆλ‹€."),
@@ -41,6 +44,7 @@ public enum SuccessStatus {
4144
DELETE_PUBLIC_COURSE_SUCCESS(HttpStatus.OK, "퍼블릭 μ½”μŠ€ μ‚­μ œμ— μ„±κ³΅ν–ˆμŠ΅λ‹ˆλ‹€."),
4245
DELETE_RECORD_SUCCESS(HttpStatus.OK, "기둝 μ‚­μ œμ— μ„±κ³΅ν–ˆμŠ΅λ‹ˆλ‹€."),
4346
DELETE_COURSES_SUCCESS(HttpStatus.OK, "μ½”μŠ€ μ‚­μ œ 성곡"),
47+
DELETE_HEALTH_DATA_SUCCESS(HttpStatus.OK, "건강 데이터 μ‚­μ œ 성곡"),
4448

4549

4650
/**
@@ -52,6 +56,7 @@ public enum SuccessStatus {
5256
CREATE_PUBLIC_COURSE_SUCCESS(HttpStatus.CREATED, "μ½”λ“œ μ—…λ‘œλ“œμ— μ„±κ³΅ν–ˆμŠ΅λ‹ˆλ‹€."),
5357
CREATE_SCRAP_SUCCESS(HttpStatus.CREATED, "μ½”μŠ€ 슀크랩 성곡"),
5458
NEW_TOKEN_SUCCESS(HttpStatus.CREATED, "토큰 μž¬λ°œκΈ‰μ— μ„±κ³΅ν–ˆμŠ΅λ‹ˆλ‹€."),
59+
CREATE_HEALTH_DATA_SUCCESS(HttpStatus.CREATED, "건강 데이터 μ €μž₯ 성곡"),
5560
;
5661

5762
private final HttpStatus httpStatus;

β€Žsrc/main/java/org/runnect/server/config/slack/SlackApi.javaβ€Ž

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,13 @@ public class SlackApi {
2727
private final static String NEW_LINE = "\n";
2828
private final static String DOUBLE_NEW_LINE = "\n\n";
2929

30-
private StringBuilder sb = new StringBuilder();
31-
3230
public void sendAlert(Exception error, HttpServletRequest request) throws IOException {
3331

3432
List<LayoutBlock> layoutBlocks = generateLayoutBlock(error, request);
3533

3634
Slack.getInstance().send(webhookUrl, WebhookPayloads
3735
.payload(p ->
38-
p.username("Exception is detected 🚨")
36+
p.username("Exception is detected \uD83D\uDEA8")
3937
.iconUrl("https://yt3.googleusercontent.com/ytc/AGIKgqMVUzRrhoo1gDQcqvPo0PxaJz7e0gqDXT0D78R5VQ=s900-c-k-c0x00ffffff-no-rj")
4038
.blocks(layoutBlocks)));
4139
}
@@ -53,16 +51,16 @@ private List<LayoutBlock> generateLayoutBlock(Exception error, HttpServletReques
5351
}
5452

5553
private String generateErrorMessage(Exception error) {
56-
sb.setLength(0);
57-
sb.append("*[πŸ”₯ Exception]*" + NEW_LINE + error.toString() + DOUBLE_NEW_LINE);
58-
sb.append("*[πŸ“© From]*" + NEW_LINE + readRootStackTrace(error) + DOUBLE_NEW_LINE);
54+
StringBuilder sb = new StringBuilder();
55+
sb.append("*[\uD83D\uDD25 Exception]*" + NEW_LINE + error.toString() + DOUBLE_NEW_LINE);
56+
sb.append("*[\uD83D\uDCE9 From]*" + NEW_LINE + readRootStackTrace(error) + DOUBLE_NEW_LINE);
5957

6058
return sb.toString();
6159
}
6260

6361
private String generateErrorPointMessage(HttpServletRequest request) {
64-
sb.setLength(0);
65-
sb.append("*[πŸ§Ύμ„ΈλΆ€μ •λ³΄]*" + NEW_LINE);
62+
StringBuilder sb = new StringBuilder();
63+
sb.append("*[\uD83E\uDDFE세뢀정보]*" + NEW_LINE);
6664
sb.append("Request URL : " + request.getRequestURL().toString() + NEW_LINE);
6765
sb.append("Request Method : " + request.getMethod() + NEW_LINE);
6866
sb.append("Request Time : " + new Date() + NEW_LINE);
@@ -71,6 +69,9 @@ private String generateErrorPointMessage(HttpServletRequest request) {
7169
}
7270

7371
private String readRootStackTrace(Exception error) {
72+
if (error.getStackTrace() == null || error.getStackTrace().length == 0) {
73+
return "Unknown";
74+
}
7475
return error.getStackTrace()[0].toString();
7576
}
7677

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package org.runnect.server.health.controller;
2+
3+
import javax.validation.Valid;
4+
import lombok.RequiredArgsConstructor;
5+
import org.runnect.server.common.constant.SuccessStatus;
6+
import org.runnect.server.common.dto.ApiResponseDto;
7+
import org.runnect.server.common.resolver.userId.UserId;
8+
import org.runnect.server.health.dto.request.HealthDataRequestDto;
9+
import org.runnect.server.health.dto.response.CreateHealthDataResponseDto;
10+
import org.runnect.server.health.dto.response.GetHealthDataResponseDto;
11+
import org.runnect.server.health.dto.response.GetHealthSummaryResponseDto;
12+
import org.runnect.server.health.service.HealthService;
13+
import org.springframework.http.HttpStatus;
14+
import org.springframework.web.bind.annotation.DeleteMapping;
15+
import org.springframework.web.bind.annotation.GetMapping;
16+
import org.springframework.web.bind.annotation.PathVariable;
17+
import org.springframework.web.bind.annotation.PostMapping;
18+
import org.springframework.web.bind.annotation.RequestBody;
19+
import org.springframework.web.bind.annotation.RequestMapping;
20+
import org.springframework.web.bind.annotation.RequestParam;
21+
import org.springframework.web.bind.annotation.ResponseStatus;
22+
import org.springframework.web.bind.annotation.RestController;
23+
24+
@RestController
25+
@RequiredArgsConstructor
26+
@RequestMapping("/api")
27+
public class HealthController {
28+
29+
private final HealthService healthService;
30+
31+
@PostMapping("record/{recordId}/health")
32+
@ResponseStatus(HttpStatus.CREATED)
33+
public ApiResponseDto<CreateHealthDataResponseDto> createHealthData(
34+
@UserId Long userId,
35+
@PathVariable(name = "recordId") Long recordId,
36+
@RequestBody @Valid final HealthDataRequestDto request) {
37+
return ApiResponseDto.success(SuccessStatus.CREATE_HEALTH_DATA_SUCCESS,
38+
healthService.createHealthData(userId, recordId, request));
39+
}
40+
41+
@GetMapping("record/{recordId}/health")
42+
@ResponseStatus(HttpStatus.OK)
43+
public ApiResponseDto<GetHealthDataResponseDto> getHealthData(
44+
@UserId Long userId,
45+
@PathVariable(name = "recordId") Long recordId) {
46+
return ApiResponseDto.success(SuccessStatus.GET_HEALTH_DATA_SUCCESS,
47+
healthService.getHealthData(userId, recordId));
48+
}
49+
50+
@GetMapping("health/summary")
51+
@ResponseStatus(HttpStatus.OK)
52+
public ApiResponseDto<GetHealthSummaryResponseDto> getHealthSummary(
53+
@UserId Long userId,
54+
@RequestParam(name = "startDate") String startDate,
55+
@RequestParam(name = "endDate") String endDate) {
56+
return ApiResponseDto.success(SuccessStatus.GET_HEALTH_SUMMARY_SUCCESS,
57+
healthService.getHealthSummary(userId, startDate, endDate));
58+
}
59+
60+
@DeleteMapping("record/{recordId}/health")
61+
@ResponseStatus(HttpStatus.OK)
62+
public ApiResponseDto deleteHealthData(
63+
@UserId Long userId,
64+
@PathVariable(name = "recordId") Long recordId) {
65+
healthService.deleteHealthData(userId, recordId);
66+
return ApiResponseDto.success(SuccessStatus.DELETE_HEALTH_DATA_SUCCESS);
67+
}
68+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package org.runnect.server.health.dto.request;
2+
3+
import java.util.List;
4+
import javax.validation.Valid;
5+
import javax.validation.constraints.NotNull;
6+
import lombok.AccessLevel;
7+
import lombok.AllArgsConstructor;
8+
import lombok.Getter;
9+
import lombok.NoArgsConstructor;
10+
11+
@Getter
12+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
13+
@AllArgsConstructor
14+
public class HealthDataRequestDto {
15+
@NotNull
16+
private Double avgHeartRate;
17+
18+
private Double maxHeartRate;
19+
20+
private Double minHeartRate;
21+
22+
@NotNull
23+
private Double calories;
24+
25+
@NotNull
26+
private Integer zone1Seconds;
27+
28+
@NotNull
29+
private Integer zone2Seconds;
30+
31+
@NotNull
32+
private Integer zone3Seconds;
33+
34+
@NotNull
35+
private Integer zone4Seconds;
36+
37+
@NotNull
38+
private Integer zone5Seconds;
39+
40+
private Double maxHeartRateConfig;
41+
42+
@Valid
43+
private List<HeartRateSampleRequestDto> heartRateSamples;
44+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package org.runnect.server.health.dto.request;
2+
3+
import javax.validation.constraints.Max;
4+
import javax.validation.constraints.Min;
5+
import javax.validation.constraints.NotNull;
6+
import lombok.AccessLevel;
7+
import lombok.AllArgsConstructor;
8+
import lombok.Getter;
9+
import lombok.NoArgsConstructor;
10+
11+
@Getter
12+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
13+
@AllArgsConstructor
14+
public class HeartRateSampleRequestDto {
15+
@NotNull
16+
private Double heartRate;
17+
18+
@NotNull
19+
private Integer elapsedSeconds;
20+
21+
@NotNull
22+
@Min(1) @Max(5)
23+
private Integer zone;
24+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.runnect.server.health.dto.response;
2+
3+
import lombok.AccessLevel;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
8+
@Getter
9+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
10+
@AllArgsConstructor(access = AccessLevel.PRIVATE)
11+
public class CreateHealthDataResponseDto {
12+
private Long healthDataId;
13+
14+
public static CreateHealthDataResponseDto of(Long healthDataId) {
15+
return new CreateHealthDataResponseDto(healthDataId);
16+
}
17+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package org.runnect.server.health.dto.response;
2+
3+
import java.util.List;
4+
import lombok.AccessLevel;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
9+
@Getter
10+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
11+
@AllArgsConstructor(access = AccessLevel.PRIVATE)
12+
public class GetHealthDataResponseDto {
13+
private HealthDataDetailResponse healthData;
14+
15+
public static GetHealthDataResponseDto of(HealthDataDetailResponse healthData) {
16+
return new GetHealthDataResponseDto(healthData);
17+
}
18+
19+
@Getter
20+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
21+
@AllArgsConstructor(access = AccessLevel.PRIVATE)
22+
public static class HealthDataDetailResponse {
23+
private Long id;
24+
private Long recordId;
25+
private Double avgHeartRate;
26+
private Double maxHeartRate;
27+
private Double minHeartRate;
28+
private Double calories;
29+
private ZoneResponse zones;
30+
private Double maxHeartRateConfig;
31+
private List<HeartRateSampleResponse> heartRateSamples;
32+
33+
public static HealthDataDetailResponse of(Long id, Long recordId, Double avgHeartRate,
34+
Double maxHeartRate, Double minHeartRate, Double calories,
35+
ZoneResponse zones, Double maxHeartRateConfig,
36+
List<HeartRateSampleResponse> heartRateSamples) {
37+
return new HealthDataDetailResponse(id, recordId, avgHeartRate, maxHeartRate,
38+
minHeartRate, calories, zones, maxHeartRateConfig, heartRateSamples);
39+
}
40+
}
41+
42+
@Getter
43+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
44+
@AllArgsConstructor(access = AccessLevel.PRIVATE)
45+
public static class ZoneResponse {
46+
private Integer zone1Seconds;
47+
private Integer zone2Seconds;
48+
private Integer zone3Seconds;
49+
private Integer zone4Seconds;
50+
private Integer zone5Seconds;
51+
52+
public static ZoneResponse of(Integer zone1Seconds, Integer zone2Seconds,
53+
Integer zone3Seconds, Integer zone4Seconds, Integer zone5Seconds) {
54+
return new ZoneResponse(zone1Seconds, zone2Seconds, zone3Seconds, zone4Seconds, zone5Seconds);
55+
}
56+
}
57+
58+
@Getter
59+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
60+
@AllArgsConstructor(access = AccessLevel.PRIVATE)
61+
public static class HeartRateSampleResponse {
62+
private Double heartRate;
63+
private Integer elapsedSeconds;
64+
private Integer zone;
65+
66+
public static HeartRateSampleResponse of(Double heartRate, Integer elapsedSeconds, Integer zone) {
67+
return new HeartRateSampleResponse(heartRate, elapsedSeconds, zone);
68+
}
69+
}
70+
}

0 commit comments

Comments
Β (0)