Skip to content

Commit 1c87478

Browse files
authored
Merge pull request #122 from SEMOSAN/feat/#107-tracking-difficulty-feedbac
[Feat] 트래킹 안내난이도 후기 등록 추가
2 parents 88ea57d + 98ead33 commit 1c87478

13 files changed

Lines changed: 557 additions & 2 deletions

File tree

src/main/java/com/semosan/api/common/status/ErrorStatus.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,10 @@ public enum ErrorStatus implements BaseStatus {
103103
/**
104104
* Hiking (등산 기록)
105105
*/
106+
HIKING_RECORD_COURSE_REQUIRED(HttpStatus.BAD_REQUEST, "HIKE_400_1", "코스 기반 등산 기록에만 난이도 피드백을 남길 수 있습니다."),
106107
HIKING_RECORD_FORBIDDEN(HttpStatus.FORBIDDEN, "HIKE_403_1", "본인이 참여한 등산 기록만 공유할 수 있습니다."),
107108
HIKING_RECORD_NOT_FOUND(HttpStatus.NOT_FOUND, "HIKE_404_1", "등산 기록을 찾을 수 없습니다."),
109+
COURSE_DIFFICULTY_FEEDBACK_ALREADY_EXISTS(HttpStatus.CONFLICT, "HIKE_409_1", "이미 난이도 피드백을 남긴 등산 기록입니다."),
108110

109111
/**
110112
* Tracking (트래킹 세션)

src/main/java/com/semosan/api/common/status/SuccessStatus.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public enum SuccessStatus implements BaseStatus {
5151
GET_HIKING_RECORD_SUMMARY_SUCCESS(HttpStatus.OK, "HIKING_200_2", "나의 등산 기록 요약 조회에 성공했습니다."),
5252
GET_HIKING_RECORD_LIST_SUCCESS(HttpStatus.OK, "HIKING_200_3", "나의 등산 기록 목록 조회에 성공했습니다."),
5353
GET_HIKING_RECORD_LIST_BY_MOUNTAIN_SUCCESS(HttpStatus.OK, "HIKING_200_4", "특정 산의 나의 등산 기록 목록 조회에 성공했습니다."),
54+
CREATE_COURSE_DIFFICULTY_FEEDBACK_SUCCESS(HttpStatus.CREATED, "HIKING_201_1", "코스 난이도 피드백 저장에 성공했습니다."),
5455

5556
/**
5657
* Tracking — 진입 화면 (#45) & 세션 (#18)

src/main/java/com/semosan/api/domain/hiking/controller/HikingRecordController.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,22 @@
44
import com.semosan.api.common.response.PageResponse;
55
import com.semosan.api.common.status.SuccessStatus;
66
import com.semosan.api.domain.hiking.controller.docs.HikingRecordControllerDocs;
7+
import com.semosan.api.domain.hiking.dto.request.CreateCourseDifficultyFeedbackRequest;
8+
import com.semosan.api.domain.hiking.dto.response.CourseDifficultyFeedbackResponse;
79
import com.semosan.api.domain.hiking.dto.response.GetUserHikingRecordResponse;
810
import com.semosan.api.domain.hiking.dto.response.GetUserHikingMountainRecordResponse;
911
import com.semosan.api.domain.hiking.dto.response.GetUserHikingRecordSummaryResponse;
1012
import com.semosan.api.domain.hiking.service.HikingRecordService;
13+
import jakarta.validation.Valid;
1114
import lombok.RequiredArgsConstructor;
1215
import org.springframework.data.domain.Pageable;
1316
import org.springframework.data.web.PageableDefault;
1417
import org.springframework.http.ResponseEntity;
1518
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1619
import org.springframework.web.bind.annotation.GetMapping;
1720
import org.springframework.web.bind.annotation.PathVariable;
21+
import org.springframework.web.bind.annotation.PostMapping;
22+
import org.springframework.web.bind.annotation.RequestBody;
1823
import org.springframework.web.bind.annotation.RequestMapping;
1924
import org.springframework.web.bind.annotation.RestController;
2025

@@ -70,4 +75,20 @@ public ResponseEntity<ApiResponse<GetUserHikingRecordSummaryResponse>> getUserHi
7075
GetUserHikingRecordSummaryResponse response = hikingRecordService.getUserHikingRecordSummary(userId);
7176
return ApiResponse.success(SuccessStatus.GET_HIKING_RECORD_SUMMARY_SUCCESS, response);
7277
}
78+
79+
// 코스 난이도 피드백을 저장합니다.
80+
@PostMapping("/{hikingRecordId}/difficulty-feedback")
81+
@Override
82+
public ResponseEntity<ApiResponse<CourseDifficultyFeedbackResponse>> createCourseDifficultyFeedback(
83+
@AuthenticationPrincipal Long userId,
84+
@PathVariable Long hikingRecordId,
85+
@Valid @RequestBody CreateCourseDifficultyFeedbackRequest request
86+
) {
87+
CourseDifficultyFeedbackResponse response = hikingRecordService.createCourseDifficultyFeedback(
88+
userId,
89+
hikingRecordId,
90+
request
91+
);
92+
return ApiResponse.success(SuccessStatus.CREATE_COURSE_DIFFICULTY_FEEDBACK_SUCCESS, response);
93+
}
7394
}

src/main/java/com/semosan/api/domain/hiking/controller/docs/HikingRecordControllerDocs.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
import com.semosan.api.common.response.ApiResponse;
44
import com.semosan.api.common.response.PageResponse;
5+
import com.semosan.api.domain.hiking.dto.request.CreateCourseDifficultyFeedbackRequest;
6+
import com.semosan.api.domain.hiking.dto.response.CourseDifficultyFeedbackResponse;
57
import com.semosan.api.domain.hiking.dto.response.GetUserHikingRecordResponse;
68
import com.semosan.api.domain.hiking.dto.response.GetUserHikingMountainRecordResponse;
79
import com.semosan.api.domain.hiking.dto.response.GetUserHikingRecordSummaryResponse;
10+
import jakarta.validation.Valid;
811
import io.swagger.v3.oas.annotations.Operation;
912
import io.swagger.v3.oas.annotations.Parameter;
1013
import io.swagger.v3.oas.annotations.media.Content;
@@ -16,6 +19,7 @@
1619
import org.springframework.http.ResponseEntity;
1720
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1821
import org.springframework.web.bind.annotation.PathVariable;
22+
import org.springframework.web.bind.annotation.RequestBody;
1923

2024
@Tag(name = "Hiking Record", description = "등산 기록 관련 API")
2125
public interface HikingRecordControllerDocs {
@@ -89,4 +93,42 @@ ResponseEntity<ApiResponse<GetUserHikingRecordSummaryResponse>> getUserHikingRec
8993
@Parameter(hidden = true)
9094
@AuthenticationPrincipal Long userId
9195
);
96+
97+
@Operation(
98+
summary = "코스 난이도 피드백 저장",
99+
description = "코스 기반 등산 기록 1개에 대해 사용자가 체감한 난이도 비교 피드백을 저장합니다."
100+
)
101+
@ApiResponses({
102+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
103+
responseCode = "201",
104+
description = "코스 난이도 피드백 저장 성공"
105+
),
106+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
107+
responseCode = "400",
108+
description = "자유 기록 등 코스 비교가 불가능한 등산 기록",
109+
content = @Content(schema = @Schema(implementation = ApiResponse.class))
110+
),
111+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
112+
responseCode = "403",
113+
description = "본인이 참여한 등산 기록이 아님",
114+
content = @Content(schema = @Schema(implementation = ApiResponse.class))
115+
),
116+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
117+
responseCode = "404",
118+
description = "등산 기록을 찾을 수 없음",
119+
content = @Content(schema = @Schema(implementation = ApiResponse.class))
120+
),
121+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
122+
responseCode = "409",
123+
description = "이미 난이도 피드백을 저장한 등산 기록",
124+
content = @Content(schema = @Schema(implementation = ApiResponse.class))
125+
)
126+
})
127+
ResponseEntity<ApiResponse<CourseDifficultyFeedbackResponse>> createCourseDifficultyFeedback(
128+
@Parameter(hidden = true)
129+
@AuthenticationPrincipal Long userId,
130+
@Parameter(description = "등산 기록 ID", required = true)
131+
@PathVariable Long hikingRecordId,
132+
@Valid @RequestBody CreateCourseDifficultyFeedbackRequest request
133+
);
92134
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.semosan.api.domain.hiking.dto.request;
2+
3+
import com.semosan.api.domain.hiking.enums.DifficultyFeedbackType;
4+
import jakarta.validation.constraints.NotNull;
5+
6+
public record CreateCourseDifficultyFeedbackRequest(
7+
@NotNull DifficultyFeedbackType comparison
8+
) {
9+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.semosan.api.domain.hiking.dto.response;
2+
3+
import com.semosan.api.domain.hiking.entity.CourseDifficultyFeedback;
4+
import com.semosan.api.domain.hiking.enums.DifficultyFeedbackType;
5+
import com.semosan.api.domain.mountain.enums.Difficulty;
6+
7+
public record CourseDifficultyFeedbackResponse(
8+
Long feedbackId,
9+
Long hikingRecordId,
10+
Long mountainId,
11+
String mountainName,
12+
Long courseId,
13+
String courseName,
14+
Difficulty guideDifficulty,
15+
DifficultyFeedbackType comparison
16+
) {
17+
18+
public static CourseDifficultyFeedbackResponse from(CourseDifficultyFeedback feedback) {
19+
return new CourseDifficultyFeedbackResponse(
20+
feedback.getId(),
21+
feedback.getHikingRecord().getId(),
22+
feedback.getCourse().getMountain().getId(),
23+
feedback.getCourse().getMountain().getName(),
24+
feedback.getCourse().getId(),
25+
feedback.getCourse().getName(),
26+
feedback.getGuideDifficulty(),
27+
feedback.getComparison()
28+
);
29+
}
30+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.semosan.api.domain.hiking.entity;
2+
3+
import com.semosan.api.common.base.BaseEntity;
4+
import com.semosan.api.common.exception.GeneralException;
5+
import com.semosan.api.common.status.ErrorStatus;
6+
import com.semosan.api.domain.hiking.enums.DifficultyFeedbackType;
7+
import com.semosan.api.domain.mountain.entity.Course;
8+
import com.semosan.api.domain.mountain.enums.Difficulty;
9+
import com.semosan.api.domain.user.entity.User;
10+
import jakarta.persistence.*;
11+
import lombok.*;
12+
13+
@Table(
14+
name = "course_difficulty_feedbacks",
15+
uniqueConstraints = {
16+
@UniqueConstraint(
17+
name = "uk_course_difficulty_feedback_hiking_record",
18+
columnNames = "hiking_record_id"
19+
)
20+
},
21+
indexes = {
22+
@Index(name = "idx_course_difficulty_feedback_course_id", columnList = "course_id"),
23+
@Index(name = "idx_course_difficulty_feedback_user_id", columnList = "user_id")
24+
}
25+
)
26+
@Getter
27+
@Entity
28+
@Builder(access = AccessLevel.PROTECTED)
29+
@AllArgsConstructor(access = AccessLevel.PROTECTED)
30+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
31+
public class CourseDifficultyFeedback extends BaseEntity {
32+
33+
@Id
34+
@GeneratedValue(strategy = GenerationType.IDENTITY)
35+
private Long id;
36+
37+
@OneToOne(fetch = FetchType.LAZY)
38+
@JoinColumn(name = "hiking_record_id", nullable = false)
39+
private HikingRecord hikingRecord;
40+
41+
@ManyToOne(fetch = FetchType.LAZY)
42+
@JoinColumn(name = "user_id", nullable = false)
43+
private User user;
44+
45+
@ManyToOne(fetch = FetchType.LAZY)
46+
@JoinColumn(name = "course_id", nullable = false)
47+
private Course course;
48+
49+
@Enumerated(EnumType.STRING)
50+
@Column(name = "guide_difficulty", nullable = false, length = 20)
51+
private Difficulty guideDifficulty;
52+
53+
@Enumerated(EnumType.STRING)
54+
@Column(name = "comparison", nullable = false, length = 20)
55+
private DifficultyFeedbackType comparison;
56+
57+
public static CourseDifficultyFeedback create(
58+
HikingRecord hikingRecord,
59+
User user,
60+
DifficultyFeedbackType comparison
61+
) {
62+
Course course = hikingRecord.getCourse();
63+
if (course == null) {
64+
throw new GeneralException(ErrorStatus.HIKING_RECORD_COURSE_REQUIRED);
65+
}
66+
return CourseDifficultyFeedback.builder()
67+
.hikingRecord(hikingRecord)
68+
.user(user)
69+
.course(course)
70+
.guideDifficulty(course.getDifficulty())
71+
.comparison(comparison)
72+
.build();
73+
}
74+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.semosan.api.domain.hiking.enums;
2+
3+
public enum DifficultyFeedbackType {
4+
SIMILAR,
5+
EASIER,
6+
HARDER
7+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.semosan.api.domain.hiking.repository;
2+
3+
import com.semosan.api.domain.hiking.entity.CourseDifficultyFeedback;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
public interface CourseDifficultyFeedbackRepository extends JpaRepository<CourseDifficultyFeedback, Long> {
7+
8+
boolean existsByHikingRecord_Id(Long hikingRecordId);
9+
}

src/main/java/com/semosan/api/domain/hiking/repository/HikingRecordRepository.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ Page<UserHikingMountainRecordProjection> findUserHikingMountainRecordsByUserId(
7474
FROM hiking_records hr
7575
JOIN hiking_members hm ON hm.hiking_record_id = hr.id
7676
JOIN mountains m ON m.id = hr.mountain_id
77-
JOIN courses c ON c.id = hr.course_id
77+
LEFT JOIN courses c ON c.id = hr.course_id
7878
WHERE hm.user_id = :userId
7979
ORDER BY hr.created_at DESC, hr.id DESC
8080
""",
@@ -121,7 +121,7 @@ Page<UserHikingRecordProjection> findUserHikingRecordsByUserId(
121121
FROM hiking_records hr
122122
JOIN hiking_members hm ON hm.hiking_record_id = hr.id
123123
JOIN mountains m ON m.id = hr.mountain_id
124-
JOIN courses c ON c.id = hr.course_id
124+
LEFT JOIN courses c ON c.id = hr.course_id
125125
WHERE hm.user_id = :userId AND hr.mountain_id = :mountainId
126126
ORDER BY hr.created_at DESC, hr.id DESC
127127
""",

0 commit comments

Comments
 (0)