Skip to content

[Feat] 트래킹 안내난이도 후기 등록 추가#122

Merged
pooreumjung merged 9 commits into
developfrom
feat/#107-tracking-difficulty-feedbac
May 27, 2026
Merged

[Feat] 트래킹 안내난이도 후기 등록 추가#122
pooreumjung merged 9 commits into
developfrom
feat/#107-tracking-difficulty-feedbac

Conversation

@pooreumjung

@pooreumjung pooreumjung commented May 27, 2026

Copy link
Copy Markdown
Member

🧾 요약

  • 코스 기반 등산 기록에 대한 난이도 피드백 저장 기능을 추가
  • 자유 기록 조회 누락, 중복 피드백 동시 저장 예외 처리, 피드백 테이블의 산 정보 중복 저장 문제를 해결

🔗 이슈

✨ 변경 내용

  • POST /api/hiking-records/{hikingRecordId}/difficulty-feedback API 추가
  • 피드백 값 SIMILAR, EASIER, HARDER 저장 지원
  • 등산 기록 1개당 피드백 1개만 저장되도록 unique 제약 및 서비스 검증 추가
  • 동시 요청으로 unique 제약 위반 발생 시 500 대신 중복 피드백 에러로 변환
  • 자유 기록이 등산 기록 목록 조회에서 누락되지 않도록 courses 조인을 LEFT JOIN으로 변경
  • course_difficulty_feedbacks 테이블 마이그레이션 추가
  • 피드백 엔티티에서 mountain_id 중복 저장 제거
  • 난이도 피드백 저장 서비스 테스트 추가

✅ 확인

  • 빌드 OK
  • 테스트 OK

Summary by CodeRabbit

  • New Features

    • Users can submit course difficulty feedback for a hiking record (Similar / Easier / Harder); one feedback per record. Feedback is saved persistently and returns a confirmation on success.
  • Bug Fixes / Improvements

    • Feedback can only be left for records with an associated course; duplicate submissions are prevented.
    • Hiking record lists now include records even when no course is associated.

Review Change Stack

@pooreumjung pooreumjung self-assigned this May 27, 2026
@pooreumjung pooreumjung added the enhancement New feature or request label May 27, 2026
@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c0ac4464-b613-464b-ba0d-4d52ffe3a330

📥 Commits

Reviewing files that changed from the base of the PR and between ac62631 and 98ead33.

📒 Files selected for processing (2)
  • src/main/java/com/semosan/api/domain/hiking/entity/CourseDifficultyFeedback.java
  • src/test/java/com/semosan/api/domain/hiking/service/HikingRecordServiceTest.java

📝 Walkthrough

Walkthrough

Adds end-to-end course difficulty feedback: new enum and status constants, DTOs, JPA entity, repository, DB migration, service logic with duplicate handling, controller endpoint and docs, left-join query adjustments, and comprehensive unit tests.

Changes

Course Difficulty Feedback Feature

Layer / File(s) Summary
Domain enums and status constants
src/main/java/com/semosan/api/domain/hiking/enums/DifficultyFeedbackType.java, src/main/java/com/semosan/api/common/status/ErrorStatus.java, src/main/java/com/semosan/api/common/status/SuccessStatus.java
Adds DifficultyFeedbackType (SIMILAR, EASIER, HARDER). Adds HIKING_RECORD_COURSE_REQUIRED error (400) and CREATE_COURSE_DIFFICULTY_FEEDBACK_SUCCESS success (201).
CourseDifficultyFeedback entity and factory
src/main/java/com/semosan/api/domain/hiking/entity/CourseDifficultyFeedback.java
New JPA entity course_difficulty_feedbacks with relations to HikingRecord, User, Course; string-enum columns; unique constraint on hiking_record_id; static create(...) factory that requires a course and throws HIKING_RECORD_COURSE_REQUIRED if absent.
Migration and repositories
src/main/java/com/semosan/api/domain/hiking/repository/CourseDifficultyFeedbackRepository.java, src/main/resources/db/migration/V19__create_course_difficulty_feedbacks.sql, src/main/java/com/semosan/api/domain/hiking/repository/HikingRecordRepository.java
Adds repository with existsByHikingRecord_Id. Creates DB migration with constraints and indexes. Updates HikingRecordRepository native queries to LEFT JOIN courses so records without a course are included.
API DTOs
src/main/java/com/semosan/api/domain/hiking/dto/request/CreateCourseDifficultyFeedbackRequest.java, src/main/java/com/semosan/api/domain/hiking/dto/response/CourseDifficultyFeedbackResponse.java
Adds validated request record with comparison and response record with flattened feedback, hiking, mountain, and course fields plus from(...) mapper.
Service business logic
src/main/java/com/semosan/api/domain/hiking/service/HikingRecordService.java
New createCourseDifficultyFeedback method: checks onboarding, hiking record existence, membership, course presence (HIKING_RECORD_COURSE_REQUIRED), duplicate pre-check and unique-constraint mapping to COURSE_DIFFICULTY_FEEDBACK_ALREADY_EXISTS, persists via saveAndFlush, and inspects DataIntegrityViolation causes for the configured unique key.
Controller and API documentation
src/main/java/com/semosan/api/domain/hiking/controller/HikingRecordController.java, src/main/java/com/semosan/api/domain/hiking/controller/docs/HikingRecordControllerDocs.java
Adds POST /{hikingRecordId}/difficulty-feedback endpoint accepting authenticated user, path hikingRecordId, and validated request body; delegates to service and documents multi-status responses including forbidden, course-required, and conflict.
Service test suite
src/test/java/com/semosan/api/domain/hiking/service/HikingRecordServiceTest.java
Extensive tests for success path, ownership/authorization, missing course validation, duplicate prevention, concurrent duplicate mapping, unrelated DataIntegrityViolation rethrow, factory validation, and reflection-based test helpers.

Sequence Diagram

sequenceDiagram
  participant Client
  participant HikingRecordController
  participant HikingRecordService
  participant CourseDifficultyFeedbackRepository
  participant Database
  Client->>HikingRecordController: POST /{hikingRecordId}/difficulty-feedback (comparison)
  HikingRecordController->>HikingRecordService: createCourseDifficultyFeedback(userId, hikingRecordId, request)
  HikingRecordService->>HikingRecordService: validate onboarding, membership, course presence
  HikingRecordService->>CourseDifficultyFeedbackRepository: existsByHikingRecord_Id(hikingRecordId)
  CourseDifficultyFeedbackRepository->>Database: SELECT EXISTS(...)
  alt not exists
    HikingRecordService->>CourseDifficultyFeedbackRepository: saveAndFlush(courseDifficultyFeedback)
    CourseDifficultyFeedbackRepository->>Database: INSERT course_difficulty_feedbacks ...
    Database-->>CourseDifficultyFeedbackRepository: commit
    CourseDifficultyFeedbackRepository-->>HikingRecordService: saved entity
  else exists or unique-constraint violation
    CourseDifficultyFeedbackRepository-->>HikingRecordService: DataIntegrityViolationException / exists true
  end
  HikingRecordService-->>HikingRecordController: CourseDifficultyFeedbackResponse
  HikingRecordController-->>Client: 201 Created (ApiResponse.success)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • SEMOSAN/SEMOSAN_BE#122: Directly related; contains the same course difficulty feedback feature changes (enums, controller, service, entity, migration, tests).
  • SEMOSAN/SEMOSAN_BE#54: Related change that allows HikingRecord.course to be nullable, which impacts the new course-required validation and LEFT JOIN adjustments.

Suggested reviewers

  • howooyeon

🐰 A hiking heart beats strong, feedback flows free,
I hop and note if it's SIMILAR, EASIER, or HARDER to me,
Tiny paws, big trails, each voice a trail guide light,
Saving climbs and stories, one feedback at a bite!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title '[Feat] 트래킹 안내난이도 후기 등록 추가' (Add tracking guide difficulty feedback registration) clearly summarizes the main change - implementing difficulty feedback submission for hiking records.
Linked Issues check ✅ Passed The PR fully implements the objective from issue #107 to create an API endpoint for submitting hiking difficulty feedback, including all necessary components: endpoint, validation, database schema, and comprehensive testing.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the difficulty feedback feature. The HikingRecordRepository join modification from INNER to LEFT JOIN is a necessary fix to prevent exclusion of course-less records.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#107-tracking-difficulty-feedbac

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/test/java/com/semosan/api/domain/hiking/service/HikingRecordServiceTest.java (1)

151-171: ⚡ Quick win

Add a non-duplicate integrity-violation test case.

Line 160–170 currently proves duplicate mapping for one DataIntegrityViolationException path only. Add a companion test for a non-unique integrity error (e.g., FK/NOT NULL) and assert it is not converted to COURSE_DIFFICULTY_FEEDBACK_ALREADY_EXISTS, so unrelated DB failures are not masked.

Suggested test addition
+    `@Test`
+    void createCourseDifficultyFeedbackDoesNotMapNonDuplicateIntegrityViolation() throws Exception {
+        User user = user(1L);
+        HikingRecord hikingRecord = hikingRecord(10L, course(mountain(20L, "관악산"), 30L, "과천향교 출발 코스"));
+
+        when(userReader.findCompletedOnboardingUserById(1L)).thenReturn(user);
+        when(hikingRecordRepository.findById(10L)).thenReturn(Optional.of(hikingRecord));
+        when(hikingMemberRepository.existsByHikingRecordAndUser(hikingRecord, user)).thenReturn(true);
+        when(courseDifficultyFeedbackRepository.existsByHikingRecord_Id(10L)).thenReturn(false);
+        when(courseDifficultyFeedbackRepository.saveAndFlush(any(CourseDifficultyFeedback.class)))
+                .thenThrow(new DataIntegrityViolationException("not-null violation"));
+
+        assertThatThrownBy(() -> hikingRecordService.createCourseDifficultyFeedback(
+                1L,
+                10L,
+                new CreateCourseDifficultyFeedbackRequest(DifficultyFeedbackType.SIMILAR)
+        ))
+                .isNotInstanceOfSatisfying(GeneralException.class, ex ->
+                        assertThat(ex.getErrorStatus())
+                                .isEqualTo(ErrorStatus.COURSE_DIFFICULTY_FEEDBACK_ALREADY_EXISTS));
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/semosan/api/domain/hiking/service/HikingRecordServiceTest.java`
around lines 151 - 171, Add a new unit test in HikingRecordServiceTest that
mirrors
createCourseDifficultyFeedbackThrowsConflictWhenConcurrentDuplicateSaveOccurs
but makes courseDifficultyFeedbackRepository.saveAndFlush(...) throw a
DataIntegrityViolationException with a non-duplicate message (e.g., "foreign key
constraint fails" or "not-null violation"); call
hikingRecordService.createCourseDifficultyFeedback(...) and assert the thrown
GeneralException's errorStatus is NOT
ErrorStatus.COURSE_DIFFICULTY_FEEDBACK_ALREADY_EXISTS (use
extracting("errorStatus").isNotEqualTo(...)); keep the same setup for
userReader.findCompletedOnboardingUserById, hikingRecordRepository.findById,
hikingMemberRepository.existsByHikingRecordAndUser and
courseDifficultyFeedbackRepository.existsByHikingRecord_Id to locate the test
logic near the existing createCourseDifficultyFeedback... test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/com/semosan/api/domain/hiking/entity/CourseDifficultyFeedback.java`:
- Around line 55-68: In CourseDifficultyFeedback.create add a defensive null
check for the course returned by hikingRecord.getCourse(): if course is null,
throw an IllegalArgumentException (or similar runtime exception) with a clear
message (e.g., "course must not be null") before calling course.getDifficulty();
update the create method in CourseDifficultyFeedback to validate hikingRecord
and course (referencing the create(...) method, CourseDifficultyFeedback class,
hikingRecord.getCourse(), and guideDifficulty/course.getDifficulty()) so the
entity enforces the non-null invariant.

In
`@src/main/java/com/semosan/api/domain/hiking/service/HikingRecordService.java`:
- Around line 105-109: In HikingRecordService.createCourseDifficultyFeedback,
narrow the catch for DataIntegrityViolationException by inspecting the nested
cause chain for org.hibernate.exception.ConstraintViolationException and only
map to ErrorStatus.COURSE_DIFFICULTY_FEEDBACK_ALREADY_EXISTS when the
constraintName equals "uk_course_difficulty_feedback_hiking_record" (the unique
constraint on hiking_record_id); for any other DataIntegrityViolationException
(or when the constraint name is different/missing) rethrow the original
exception (or wrap with a more general error) instead of always returning
COURSE_DIFFICULTY_FEEDBACK_ALREADY_EXISTS; update the concurrent duplicate save
test to assert the specific unique-constraint behavior rather than relying on a
generic DataIntegrityViolationException message.

---

Nitpick comments:
In
`@src/test/java/com/semosan/api/domain/hiking/service/HikingRecordServiceTest.java`:
- Around line 151-171: Add a new unit test in HikingRecordServiceTest that
mirrors
createCourseDifficultyFeedbackThrowsConflictWhenConcurrentDuplicateSaveOccurs
but makes courseDifficultyFeedbackRepository.saveAndFlush(...) throw a
DataIntegrityViolationException with a non-duplicate message (e.g., "foreign key
constraint fails" or "not-null violation"); call
hikingRecordService.createCourseDifficultyFeedback(...) and assert the thrown
GeneralException's errorStatus is NOT
ErrorStatus.COURSE_DIFFICULTY_FEEDBACK_ALREADY_EXISTS (use
extracting("errorStatus").isNotEqualTo(...)); keep the same setup for
userReader.findCompletedOnboardingUserById, hikingRecordRepository.findById,
hikingMemberRepository.existsByHikingRecordAndUser and
courseDifficultyFeedbackRepository.existsByHikingRecord_Id to locate the test
logic near the existing createCourseDifficultyFeedback... test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 46575288-f052-466d-9351-9b885a14cbd1

📥 Commits

Reviewing files that changed from the base of the PR and between 88ea57d and 5d1f9b8.

📒 Files selected for processing (13)
  • src/main/java/com/semosan/api/common/status/ErrorStatus.java
  • src/main/java/com/semosan/api/common/status/SuccessStatus.java
  • src/main/java/com/semosan/api/domain/hiking/controller/HikingRecordController.java
  • src/main/java/com/semosan/api/domain/hiking/controller/docs/HikingRecordControllerDocs.java
  • src/main/java/com/semosan/api/domain/hiking/dto/request/CreateCourseDifficultyFeedbackRequest.java
  • src/main/java/com/semosan/api/domain/hiking/dto/response/CourseDifficultyFeedbackResponse.java
  • src/main/java/com/semosan/api/domain/hiking/entity/CourseDifficultyFeedback.java
  • src/main/java/com/semosan/api/domain/hiking/enums/DifficultyFeedbackType.java
  • src/main/java/com/semosan/api/domain/hiking/repository/CourseDifficultyFeedbackRepository.java
  • src/main/java/com/semosan/api/domain/hiking/repository/HikingRecordRepository.java
  • src/main/java/com/semosan/api/domain/hiking/service/HikingRecordService.java
  • src/main/resources/db/migration/V19__create_course_difficulty_feedbacks.sql
  • src/test/java/com/semosan/api/domain/hiking/service/HikingRecordServiceTest.java

@pooreumjung
pooreumjung merged commit 1c87478 into develop May 27, 2026
@howooyeon
howooyeon deleted the feat/#107-tracking-difficulty-feedbac branch May 29, 2026 12:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 트래킹 안내난이도 API

1 participant