Skip to content

[BE-55] feat: 실시간 길안내 기능 구현#167

Closed
Woojae-Jeong wants to merge 44 commits intodevfrom
test
Closed

[BE-55] feat: 실시간 길안내 기능 구현#167
Woojae-Jeong wants to merge 44 commits intodevfrom
test

Conversation

@Woojae-Jeong
Copy link
Collaborator

@Woojae-Jeong Woojae-Jeong commented Mar 14, 2026

#️⃣연관된 이슈

#106

📝작업 내용

웹소켓을 통해 유저와 웹소켓을 연결하고 인증을 마무리하게 된다면, 길안내가 시작됩니다. 길안내 과정에서 중요한 서비스나 로직에 대해서 정리하였습니다

1. TravelService

전반적인 길안내를 담당하는 서비스입니다. 크게 동작 방식은 아래와 같습니다.

  1. 소켓으로 들어온 이벤트 이름을 확인하고, 현재 유저의 상태를 조회합니다
  2. 유저의 상태가 해당 길안내 이벤트를 처리할 수 있는 상태인지 확인합니다.
  3. 요청으로 들어온 이벤트에 맞는 핸들러를 호출하여 길안내를 수행합니다

3번 과정에서 핸들러를 호출하여 길안내를 수행할 때, 핸들러가 현재 별도로 분리되어 있지 않고, TravelService에 합쳐져 있는 상태입니다. 이는 추후에 분리할 수 있으면 분리하면 좋겠네요


산행 이벤트별 동작 안내

1. start 이벤트

해당 이벤트는 길안내를 시작하는 이벤트입니다

  1. 요청으로 들어온 body의 데이터를 바탕으로 TravelTrackingInfo 를 만들게 됩니다.
  • TravelTrackingInfo란, 말 그대로 해당 유저의 실시간 등산 정보를 담고 있는 것입니다.
  1. 이탈 여부, 도착 여부 체크, 남은 등산 시간, 총 등산 거리 계산
  • 이탈 여부 체크 : 선택한 코스의 수많은 좌표 중에서 사용자의 현재 위치와 가장 가까운 좌표를 찾게 됩니다. 이를 바탕으로, 두 좌표가 이탈 허용 거리(50m)를 넘어가게 되면 이탈 처리를 하게 됩니다
  • 도착 여부 체크 : 사용자의 현재 위치와 코스의 도착 지점의 위치간 거리가 10m 이하면 도착으로 판단, 그렇지 않으면 미도착으로 판단합니다
  • 남은 등산 시간 계산 : 사용자의 현재 위치와 경유지 및 도착지까지의 남은 거리를 평균 등산 속도인 2.16km/h 로 계산하게 됩니다
  • 총 등산 거리 계산 : 기존 총 등산 거리(여기서는 등산 시작이니 0km) 에 현재 사용자 위치까지의 거리를 더해 총 등산 거리를 업데이트
  1. TravelTrackingInfo 정보 업데이트(남은 시간)
  2. 유저에게 도착여부, 이탈여부, 총 등산 거리, 남은 등산 시간(경유지, 도착지)를 응답으로 반환

2. currentPosition 이벤트

사용자가 등산을 하는 중으로, 실시간 위치를 보내오는 이벤트입니다.

  1. 이탈 여부, 도착 여부 체크, 남은 등산 시간, 총 등산 거리 업데이트 (로직 동일)
  2. TravelTrackingInfo 정보 업데이트(남은 시간, 총 등산 거리, 등산 경로(path)에 현재 위치 추가, 상태를 TRAVEL 로 변경)
  3. 응답 반환

3. pause 이벤트

사용자가 등산 중 일시 중지를 하는 이벤트입니다.

  1. 이탈 여부, 도착 여부 체크, 남은 등산 시간, 총 등산 거리 업데이트 (로직 동일)
  2. TravelTrackingInfo 정보 업데이트(남은 시간, 총 등산 거리, 등산 경로(path)에 현재 위치 추가, 상태를 PAUSED 로 변경)
  3. 응답 반환

4. reStart 이벤트

사용자가 일시 중지 상태에서 다시 등산을 재개하는 이벤트입니다

  1. 이탈 여부, 도착 여부 체크, 남은 등산 시간, 총 등산 거리 업데이트 (로직 동일)
  2. TravelTrackingInfo 정보 업데이트(남은 시간, 총 등산 거리, 등산 경로(path)에 현재 위치 추가, 상태를 RESTARTED 로 변경)
  3. 응답 반환

5. end 이벤트

사용자가 등산 길안내를 종료하는 이벤트입니다

  1. 이탈 여부, 도착 여부 체크, 남은 등산 시간, 총 등산 거리 업데이트 (로직 동일)
  2. TravelTrackingInfo 정보 업데이트(남은 시간, 총 등산 거리, 등산 경로(path)에 현재 위치 추가, 상태를 END 로 변경, 총 등산 시간 저장, 종료 시간 저장)
  3. TravelRecord 저장 : TravelTrackingInfo를 바탕으로 TravelRecord를 생성하여 등산 기록을 저장합니다.
  4. 응답 반환

기타

등산 길안내 과정에서 핵심 로직들만 설명을 드렸습니다. 여러 보조 서비스들에 대한 설명은 하지 못했는데, 제가 이름은 잘 지어놓았고, 해당 보조 서비스들이 결국 위에 있는 핵심 로직들에서 책임 분리 측면에서 만들어낸 서비스들이라 이해하시는 데 무리가 없을 겁니다. (혹여 해당 서비스가 잘 이해가 안되신다면 다시 여쭤봐주세요)

  1. data.sql 업데이트
  • 기존 data.sql을 통해 base 초기 데이터 삽입 시 위경도를 날것으로 삽입을 하였는데, 길안내 과정에서 해당 base 정보를 활용하니 바로 활용할 수 있도록 geometry 객체 형태로 삽입되도록 수정하였습니다.
  • 더불어 course를 삽입할 때도 도착지에 대한 데이터가 없길래 destination_base_id를 추가해줬습니다
  1. TravelRecord 도메인 기능
    등산 기록을 의미하는 TravelRecord 도메인에는 간단 crud 밖에 별다른 설명은 하지 않아도 될 것 같아 생략합니다.
  • 산행 기록 조회 : 월(month)단위로 등산 기록을 조회합니다
  • 산행 기록 상세조회 : 특정 등산 기록을 상세조회합니다.
  • 산행 기록 삭제 : 특정 등산 기록을을 삭제합니다.

스크린샷 (선택)

💬리뷰 요구사항(선택)

Summary by CodeRabbit

새로운 기능

  • 실시간 산행 추적 기능 추가 - WebSocket 기반의 위치 업데이트 및 상태 관리
  • 월별 산행 기록 조회 및 상세 정보 조회 기능
  • 목적지 도착 여부 및 경로 이탈 감지 기능
  • 산행 거리 및 소요 시간 기록 및 분석

변경 사항

  • WebSocket 엔드포인트 경로 변경: "/travel" → "/travel-navigate"

Woojae-Jeong and others added 30 commits October 9, 2025 06:10
trigger 브랜치를 dev에서 test로 변경
@coderabbitai
Copy link

coderabbitai bot commented Mar 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f0f6db8a-e6f0-4edc-9244-790917fd04a5

📥 Commits

Reviewing files that changed from the base of the PR and between 6304125 and 3cc1970.

📒 Files selected for processing (2)
  • src/main/java/com/econo_4factorial/newproject/auth/jwt/service/AuthTokenService.java
  • src/main/java/com/econo_4factorial/newproject/common/annotation/resolver/UserIdResolver.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/com/econo_4factorial/newproject/common/annotation/resolver/UserIdResolver.java
  • src/main/java/com/econo_4factorial/newproject/auth/jwt/service/AuthTokenService.java

Walkthrough

지리 정보 시스템(GIS)으로 마이그레이션하면서 위도/경도를 JTS Point/LineString으로 변경합니다. 웹소켓 기반 실시간 여행 추적 및 기록 시스템을 추가하고, 거리 계산, 남은 시간 계산, 위치 편차 감지 등의 도메인 로직을 구현합니다.

Changes

Cohort / File(s) Summary
지리 정보 시스템 마이그레이션
src/main/java/com/econo_4factorial/newproject/base/domain/Base.java, src/main/java/com/econo_4factorial/newproject/base/dto/BaseDTO.java, src/main/java/com/econo_4factorial/newproject/base/service/weather/WeatherService.java, src/main/java/com/econo_4factorial/newproject/pathway/domain/Pathway.java, src/main/java/com/econo_4factorial/newproject/pathway/dto/PathwayDTO.java, src/main/java/com/econo_4factorial/newproject/course/domain/Course.java
BigDecimal 기반 위도/경도에서 JTS Point/LineString으로 변경. 데이터베이스 스키마를 POINT/LINESTRING SRID 4326로 업데이트.
경로 처리 및 DTO 리팩토링
src/main/java/com/econo_4factorial/newproject/pathway/mapper/PathwayMapper.java, src/main/java/com/econo_4factorial/newproject/pathway/dto/res/GetPathwaysOfCourseRes.java, src/main/java/com/econo_4factorial/newproject/pathway/dto/res/PathwayCoordinatesArrayDTO.java, src/main/java/com/econo_4factorial/newproject/pathway/service/CoursePathwaySequenceService.java, src/main/java/com/econo_4factorial/newproject/pathway/service/PathwayService.java
JSON 문자열 파싱 로직 제거, LineString 직접 사용으로 전환. PathwayCoordinatesArrayDTO 도입으로 좌표 배열 표현 통일.
여행 추적 및 기록 도메인
src/main/java/com/econo_4factorial/newproject/travel/domain/TravelRecord.java, src/main/java/com/econo_4factorial/newproject/travel/domain/TravelTrackingInfo.java, src/main/java/com/econo_4factorial/newproject/travel/domain/vo/RemainingTime.java, src/main/java/com/econo_4factorial/newproject/travel/Status.java, src/main/java/com/econo_4factorial/newproject/travel/TravelEvent.java
여행 기록과 추적을 위한 도메인 엔터티 및 값 객체 추가. 상태 관리(UNSTARTED, STARTED, TRAVEL, PAUSED, RESTARTED, END) 및 이벤트 정의.
여행 서비스 계층
src/main/java/com/econo_4factorial/newproject/travel/service/TravelService.java, src/main/java/com/econo_4factorial/newproject/travel/service/TravelRecordService.java, src/main/java/com/econo_4factorial/newproject/travel/service/TravelTrackingInfoStore.java, src/main/java/com/econo_4factorial/newproject/travel/service/TravelDomainService.java, src/main/java/com/econo_4factorial/newproject/travel/service/RemainingTimeCalculator.java, src/main/java/com/econo_4factorial/newproject/travel/service/TravelDistanceCalculator.java
이벤트 기반 여행 상태 관리, 거리/시간 계산, 도메인 분석 로직 구현.
여행 DTO 및 요청/응답
src/main/java/com/econo_4factorial/newproject/travel/dto/Payload.java, src/main/java/com/econo_4factorial/newproject/travel/dto/TravelAnalysisInput.java, src/main/java/com/econo_4factorial/newproject/travel/dto/TravelAnalysisResult.java, src/main/java/com/econo_4factorial/newproject/travel/dto/TravelRecordDTO.java, src/main/java/com/econo_4factorial/newproject/travel/dto/TravelRecordDetailDTO.java, src/main/java/com/econo_4factorial/newproject/travel/dto/req/*, src/main/java/com/econo_4factorial/newproject/travel/dto/res/*
웹소켓 페이로드 및 여행 이벤트별 요청/응답 DTO 추가. 상세 기록 조회, 월별 기록 조회용 응답 구조.
WebSocket 및 인증 관리
src/main/java/com/econo_4factorial/newproject/common/util/webSocket/WebSocketAuthService.java, src/main/java/com/econo_4factorial/newproject/common/util/webSocket/WebSocketResponser.java, src/main/java/com/econo_4factorial/newproject/common/util/webSocket/WebSocketSuccessRes.java, src/main/java/com/econo_4factorial/newproject/common/util/webSocket/WebSocketFailRes.java, src/main/java/com/econo_4factorial/newproject/travel/handler/WebSocketHandler.java
WebSocket 세션별 JWT 인증, 성공/실패 응답 직렬화, 핸들러 확장으로 완전한 이벤트 처리 흐름 구현.
거리 및 위치 계산
src/main/java/com/econo_4factorial/newproject/course/service/CourseLocationMatcher.java, src/main/java/com/econo_4factorial/newproject/course/dto/ClosestCoordinateInfo.java, src/main/java/com/econo_4factorial/newproject/course/repository/CourseRepository.java
사용자 위치에서 가장 가까운 경로 좌표 찾기, 목적지 도착 여부 확인 쿼리 추가.
여행 기록 컨트롤러
src/main/java/com/econo_4factorial/newproject/travel/controller/TravelRecordController.java
월별 기록 조회, 상세 기록 조회, 기록 삭제 REST 엔드포인트 추가.
예외 처리 및 에러 타입
src/main/java/com/econo_4factorial/newproject/travel/exception/TravelErrorType.java, src/main/java/com/econo_4factorial/newproject/travel/exception/*, src/main/java/com/econo_4factorial/newproject/course/exception/CourseErrorType.java, src/main/java/com/econo_4factorial/newproject/course/exception/.../ClosestCoordinateNotFoundException.java, src/main/java/com/econo_4factorial/newproject/common/exception/CommonErrorType.java, src/main/java/com/econo_4factorial/newproject/common/exception/WebSocketIOException.java
여행 및 경로 관련 에러 타입 정의, WebSocket IO 예외 추가.
유틸리티 및 설정
src/main/java/com/econo_4factorial/newproject/travel/util/EventPolicy.java, src/main/java/com/econo_4factorial/newproject/travel/util/GeoUtil.java, src/main/java/com/econo_4factorial/newproject/travel/util/PayloadMapper.java, src/main/java/com/econo_4factorial/newproject/travel/util/TravelResponseMapper.java, src/main/java/com/econo_4factorial/newproject/travel/mapper/TravelMapper.java, src/main/java/com/econo_4factorial/newproject/common/util/DateUtil.java, src/main/java/com/econo_4factorial/newproject/common/util/TimeMapper.java, src/main/java/com/econo_4factorial/newproject/common/config/GeometryConfig.java
여행 이벤트 정책, 지리 변환, 페이로드 맵핑, 날짜/시간 유틸리티 및 JTS GeometryFactory 빈 설정.
기타 변경
.github/workflows/trigger-cicd.yml, build.gradle, src/main/resources/application-jpa.yml, src/main/resources/data.sql, src/main/java/com/econo_4factorial/newproject/auth/jwt/service/AuthTokenService.java, src/main/java/com/econo_4factorial/newproject/common/annotation/resolver/UserIdResolver.java, src/main/java/com/econo_4factorial/newproject/common/config/WebSocketConfig.java
Gradle 의존성 추가(Hibernate Spatial, JTS), 로깅 지원 추가, WebSocket 엔드포인트 경로 변경, 데이터 마이그레이션.

Sequence Diagram

sequenceDiagram
    participant WebSocket Client
    participant WebSocketHandler
    participant WebSocketAuthService
    participant TravelService
    participant TravelTrackingInfoStore
    participant TravelDomainService
    participant WebSocketResponser

    WebSocket Client->>WebSocketHandler: 연결 수립
    WebSocketHandler->>WebSocketHandler: 세션 저장
    
    WebSocket Client->>WebSocketHandler: AUTH 이벤트 (JWT 토큰)
    WebSocketHandler->>WebSocketAuthService: handleAuthEvent(payload, session)
    WebSocketAuthService->>WebSocketAuthService: JWT 검증 및 userId 추출
    WebSocketAuthService->>WebSocketAuthService: 세션-userId 매핑 저장
    WebSocketAuthService-->>WebSocketHandler: 성공
    
    WebSocket Client->>WebSocketHandler: START 이벤트 (courseId, 좌표)
    WebSocketHandler->>WebSocketHandler: 인증 세션 확인
    WebSocketHandler->>TravelService: execute(payload, userId)
    TravelService->>TravelTrackingInfoStore: makeInfo(...) 및 start()
    TravelService->>TravelDomainService: analyzeTravelStatus(...)
    TravelDomainService->>TravelDomainService: 거리, 남은시간, 편차 계산
    TravelDomainService-->>TravelService: TravelAnalysisResult
    TravelService-->>WebSocketHandler: TravelEventResponse
    WebSocketHandler->>WebSocketResponser: success(session, event, data)
    WebSocketResponser->>WebSocket Client: JSON 응답 전송
    
    WebSocket Client->>WebSocketHandler: CURRENT_POSITION 이벤트
    WebSocketHandler->>TravelService: execute(payload, userId)
    TravelService->>TravelDomainService: analyzeTravelStatus(...)
    TravelDomainService->>TravelDomainService: 현재 위치 분석
    TravelService->>TravelTrackingInfoStore: currentPosition() 또는 스킵
    TravelService-->>WebSocketHandler: TravelEventResponse
    WebSocketHandler->>WebSocketResponser: success(session, event, data)
    WebSocketResponser->>WebSocket Client: 위치 업데이트 응답
    
    WebSocket Client->>WebSocketHandler: END 이벤트
    WebSocketHandler->>TravelService: execute(payload, userId)
    TravelService->>TravelDomainService: analyzeTravelStatus(...)
    TravelService->>TravelTrackingInfoStore: end(...)
    TravelService->>TravelRecordService: saveTravelRecord(info)
    TravelRecordService->>TravelRecordService: TravelRecord 저장
    TravelService->>TravelTrackingInfoStore: deleteInfo(userId)
    TravelService-->>WebSocketHandler: TravelEventResponse
    WebSocketHandler->>WebSocketResponser: success(session, event, data)
    WebSocketResponser->>WebSocket Client: 종료 응답
    
    WebSocket Client->>WebSocketHandler: 연결 종료
    WebSocketHandler->>WebSocketAuthService: removeSession(session)
    WebSocketHandler->>TravelService: deleteIfExistTravelTrackingInfo(userId)
    WebSocketHandler->>WebSocketHandler: 세션 정리
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

이 변경사항은 광범위한 도메인 추가(여행 추적, GIS 마이그레이션), 복잡한 상태 관리(7개 상태, 6개 이벤트), WebSocket 실시간 통신, 다층 거리/시간 계산 로직, 다수의 새로운 서비스/맵퍼/DTO(50개 이상의 새 파일)를 포함합니다. 각 컴포넌트 간 상호작용이 밀집되어 있고, 기하학적 계산의 정확성, 상태 전이 유효성, 데이터 일관성 검증이 필수적입니다.

Poem

🐰 새로운 여행의 길을 닦으며,
점과 선으로 산을 그리고,
WebSocket 파도 타며 위치를 추적해,
거리와 시간을 계산해 내는 우리,
모험의 기록을 남기고 떠나간다! 🏔️✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 주요 변경사항인 실시간 길안내 기능 구현을 명확하게 요약하고 있으며, 이슈 번호(BE-55)를 포함하고 있습니다.
Description check ✅ Passed PR 설명은 템플릿의 연관 이슈, 작업 내용을 충실히 포함하고 있으며, 핵심 서비스(TravelService)와 5가지 이벤트별 동작, 기타 변경사항을 상세히 기술하고 있습니다.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch test
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (15)
.github/workflows/trigger-cicd.yml-4-4 (1)

4-4: ⚠️ Potential issue | 🟠 Major

트리거 브랜치(test)와 디스패치 이벤트 타입(backend-dev)의 불일치를 수정해 주세요.

현재 repo의 다른 워크플로우들을 보면:

  • back-dev.yml: dev 브랜치 push → AWS ECS 배포 (dev 환경)
  • onprem-dev.yml: dev 브랜치 push → Raspberry Pi 배포 (dev 환경)

하지만 trigger-cicd.yml은:

  • test 브랜치 push → backend-dev 이벤트 발행

test 브랜치의 목적이 명확하지 않고, 이벤트 타입 backend-dev의 "dev"는 실제 배포 환경(dev)을 의미하는 것으로 보여 혼동의 여지가 있습니다. 브랜치 전략을 명확히 하고 다음 중 하나로 일관성 있게 수정하세요:

🔧 정합성 맞추기 예시 (둘 중 하나 선택)

옵션 1: 브랜치를 dev로 변경

 on:
   push:
-    branches: [ test ]
+    branches: [ dev ]

옵션 2: 이벤트 타입을 backend-test로 변경

       - name: Call Infra repo workflow
         uses: peter-evans/repository-dispatch@v3
         with:
-          event-type: backend-dev
+          event-type: backend-test
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/trigger-cicd.yml at line 4, The trigger in
trigger-cicd.yml currently uses branches: [ test ] while dispatches an event
type backend-dev; update for consistency with existing workflows by either
changing the branch from "test" to "dev" (so branches: [ dev ]) or renaming the
dispatched event type from "backend-dev" to "backend-test" so the branch and
event meaning align; ensure the branch name and the repository_dispatch
event_type (backend-dev or backend-test) are consistent with back-dev.yml and
onprem-dev.yml conventions.
src/main/java/com/econo_4factorial/newproject/common/exception/CommonErrorType.java-12-12 (1)

12-12: ⚠️ Potential issue | 🟠 Major

에러 코드와 HTTP 상태가 불일치합니다.

Line 12에서 WEB_SOCKET500인데 HttpStatus.BAD_REQUEST로 매핑되어 에러 분류가 어긋납니다. 서버 측 소켓 I/O 예외라면 5xx로 맞추는 게 안전합니다.

패치 제안
-    WEB_SOCKET_IO_EXCEPTION("WEB_SOCKET500", HttpStatus.BAD_REQUEST, "소켓 IOException 발생"),
+    WEB_SOCKET_IO_EXCEPTION("WEB_SOCKET500", HttpStatus.INTERNAL_SERVER_ERROR, "소켓 IOException 발생"),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/common/exception/CommonErrorType.java`
at line 12, The enum constant WEB_SOCKET_IO_EXCEPTION in CommonErrorType uses
code "WEB_SOCKET500" but is mapped to HttpStatus.BAD_REQUEST; change its
HttpStatus to a 5xx (e.g., HttpStatus.INTERNAL_SERVER_ERROR) so the status
matches the "500" code. Locate the CommonErrorType enum and update the
WEB_SOCKET_IO_EXCEPTION entry to use the correct HttpStatus (and run tests or
compile to ensure no other logic depends on BAD_REQUEST).
src/main/java/com/econo_4factorial/newproject/travel/dto/req/EndEventReq.java-3-8 (1)

3-8: ⚠️ Potential issue | 🟠 Major

입력 DTO 검증이 없어 런타임 오류를 유발할 수 있습니다.

Line 4~7은 좌표 길이, null, 음수 시간 값이 그대로 통과됩니다. 최소 검증을 DTO 경계에서 막아야 안전합니다.

패치 제안
 public record EndEventReq(
         double[] coordinate,
         Long courseId,
         Long time,
         Long totalTravelTime
 ) {
+    public EndEventReq {
+        if (coordinate == null || coordinate.length != 2) {
+            throw new IllegalArgumentException("coordinate must have exactly 2 elements [lng, lat]");
+        }
+        if (courseId == null || time == null || totalTravelTime == null) {
+            throw new IllegalArgumentException("courseId, time, totalTravelTime must not be null");
+        }
+        if (time < 0 || totalTravelTime < 0) {
+            throw new IllegalArgumentException("time values must be non-negative");
+        }
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/travel/dto/req/EndEventReq.java`
around lines 3 - 8, EndEventReq currently allows nulls, wrong coordinate length,
and negative times; add validation in the record by implementing a compact
canonical constructor for EndEventReq that checks: coordinate is not null and
coordinate.length == 2 (and its elements are finite if needed), courseId is not
null, time and totalTravelTime are not null and >= 0; on violation throw an
IllegalArgumentException (or a domain-specific exception) with a clear message
so invalid DTOs are rejected at creation time.
src/main/java/com/econo_4factorial/newproject/travel/dto/req/RestartEventReq.java-3-9 (1)

3-9: ⚠️ Potential issue | 🟠 Major

이벤트 시간 타입이 다른 요청 DTO와 불일치합니다.

RestartEventReqtime 필드가 Timestamp 타입인데, StartEventReqEndEventReqLong 타입을 사용합니다. PauseEventReq도 동일하게 Timestamp를 사용하고 있어 일관성이 부족합니다. 시스템의 TimeMapper.toLocalTime() 유틸리티는 Long 타입을 기대하므로, 역직렬화 실패나 타입 변환 오류를 방지하려면 Long 타입으로 통일하세요.

패치 제안
-import java.sql.Timestamp;
-
 public record RestartEventReq(
         double[] coordinate,
         Long courseId,
-        Timestamp time
+        Long time
 ) {
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/travel/dto/req/RestartEventReq.java`
around lines 3 - 9, The RestartEventReq record uses java.sql.Timestamp for the
time field which is inconsistent with StartEventReq and EndEventReq (which use
Long) and incompatible with TimeMapper.toLocalTime(); change the RestartEventReq
time component from Timestamp to Long, remove the unused import, and update any
callers or deserialization expectations that reference RestartEventReq.time so
they pass a Long (or convert to Long) to match StartEventReq/EndEventReq and
work with TimeMapper.toLocalTime().
src/main/java/com/econo_4factorial/newproject/common/annotation/resolver/UserIdResolver.java-38-38 (1)

38-38: ⚠️ Potential issue | 🟠 Major

보안 위험: JWT 토큰을 로그에 기록하지 마세요.

JWT 토큰은 사용자를 인증하는 민감한 자격 증명입니다. 로그에 토큰을 기록하면 로그가 노출될 경우 토큰 탈취 및 사용자 사칭 공격에 악용될 수 있습니다.

토큰 자체 대신 토큰 존재 여부나 마스킹된 값만 로깅하세요.

🔒 제안된 수정
        String token = jwtTokenProvider.extractToken(header);
-        log.info("토큰 추출 완료. 토큰 : {}", token);
+        log.info("토큰 추출 완료.");
        return jwtTokenProvider.getUserIdFromAccessToken(token);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/common/annotation/resolver/UserIdResolver.java`
at line 38, In UserIdResolver (the class/method that currently logs the raw JWT
with log.info("토큰 추출 완료. 토큰 : {}", token)), remove the raw token from logs and
replace it with a non-sensitive message — either log only the presence (e.g.,
"토큰 존재") or a safely masked token (e.g., show only first/last few chars or
fixed-length mask) — and ensure any existing log statement is updated
accordingly so no full JWT is ever written to logs.
src/main/java/com/econo_4factorial/newproject/travel/dto/TravelRecordDetailDTO.java-23-23 (1)

23-23: ⚠️ Potential issue | 🟠 Major

종료 시간이 시작 시간으로 잘못 매핑됩니다.

Line [23]에서 endAtstartedAt으로 변환하고 있어, 상세 응답의 종료 시각이 항상 잘못 내려갑니다. 종료 시각 필드로 매핑해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/travel/dto/TravelRecordDetailDTO.java`
at line 23, The endAt variable in TravelRecordDetailDTO is incorrectly computed
from travelRecord.getStartedAt(); change the mapping to use the travelRecord's
end-time getter (e.g., travelRecord.getEndedAt() or getEndAt() depending on the
domain model) when building endAt (Timestamp.valueOf(...).getTime()) so the
DTO's endAt reflects the actual travel end time.
src/main/java/com/econo_4factorial/newproject/course/domain/Course.java-50-51 (1)

50-51: ⚠️ Potential issue | 🟠 Major

코스 좌표 컬럼에 NOT NULL 제약을 추가하세요.

Course.coordinates가 nullable 상태로 방치되어 있어 데이터 무결성이 보장되지 않습니다. 유사한 지리 데이터 필드인 Pathway.coordinatesTravelRecord.paths는 모두 nullable = false로 정의되어 있으므로, 일관성 있는 엔티티 설계를 위해 동일한 제약을 적용해야 합니다.

제안 수정안
-    `@Column`(columnDefinition = "LINESTRING SRID 4326")
+    `@Column`(nullable = false, columnDefinition = "LINESTRING SRID 4326")
     private LineString coordinates;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/econo_4factorial/newproject/course/domain/Course.java`
around lines 50 - 51, The Course.coordinates field lacks a NOT NULL constraint;
update the JPA mapping for Course.coordinates by setting nullable = false on its
`@Column` (preserving the existing columnDefinition = "LINESTRING SRID 4326") so
Course.coordinates matches Pathway.coordinates and TravelRecord.paths and
enforces non-null at the schema level.
src/main/java/com/econo_4factorial/newproject/travel/dto/req/PauseEventReq.java-3-8 (1)

3-8: ⚠️ Potential issue | 🟠 Major

Timestamp + double[] 조합으로 인한 불변성/직렬화 안정성 약화 — RestartEventReq도 동일 문제

PauseEventReqRestartEventReq 모두 Timestamp timedouble[] coordinate를 사용 중입니다. timeInstant 또는 Long으로, coordinate는 불변 컬렉션으로 변경하는 것이 안전합니다.

현재 다른 EventReq 클래스들(StartEventReq, EndEventReq)은 Long time을 사용하고 있으므로 일관성을 맞추어야 합니다.

🔧 제안 수정안
-import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.List;

 public record PauseEventReq(
-        double[] coordinate,
+        List<Double> coordinate,
         Long courseId,
-        Timestamp time
+        Instant time
 ) {
 }

또는 double[]List<Double> 대신 불변 값 객체(예: Coordinate 클래스)를 사용하는 것도 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/travel/dto/req/PauseEventReq.java`
around lines 3 - 8, PauseEventReq and RestartEventReq use mutable types
(double[] coordinate and java.sql.Timestamp time) which weakens immutability and
serialization consistency; change the record components to use a stable time
type (Long time to match StartEventReq/EndEventReq or java.time.Instant) and
replace double[] coordinate with an immutable value (e.g., List<Double> or
better a small immutable Coordinate value object) so the records remain truly
immutable and consistent with other EventReq classes; update the record
declarations PauseEventReq and RestartEventReq and any callers/serializers to
accept the new types.
src/main/java/com/econo_4factorial/newproject/travel/util/EventPolicy.java-22-24 (1)

22-24: ⚠️ Potential issue | 🟠 Major

isAllowed 메서드에서 NPE 발생 가능

policyMap에 정의되지 않은 Status(예: Status.END)로 호출 시 policyMap.get(status)null을 반환하고, null.contains(event)에서 NullPointerException이 발생합니다.

🐛 제안된 수정
 public boolean isAllowed(Status status, TravelEvent event) {
-    return policyMap.get(status).contains(event);
+    Set<TravelEvent> allowedEvents = policyMap.get(status);
+    return allowedEvents != null && allowedEvents.contains(event);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/econo_4factorial/newproject/travel/util/EventPolicy.java`
around lines 22 - 24, isAllowed 메서드 in EventPolicy can throw NPE when
policyMap.get(status) is null (e.g., Status.END); update isAllowed to handle
missing keys safely by checking policyMap.containsKey(status) or using a
null-safe lookup (e.g., getOrDefault) and return false when no set exists so
policyMap and status-related keys (policyMap, isAllowed, Status) never cause a
NullPointerException.
src/main/java/com/econo_4factorial/newproject/course/service/CourseService.java-64-69 (1)

64-69: ⚠️ Potential issue | 🟠 Major

Long 객체 비교 시 == 사용 - 잠재적 버그

result == TRUE는 Long 객체 간의 참조 비교를 수행합니다. result가 새로운 Long 객체로 반환되면 값이 같아도 false를 반환할 수 있습니다. Long 캐시 범위(-128~127) 내의 값이라 현재는 동작하겠지만, .equals() 또는 primitive 비교를 사용하는 것이 안전합니다.

🐛 제안하는 수정
     public Boolean isArrived(Long courseId, Point userPoint) {
         long result = courseRepository.isUserArrivedDestination(courseId, userPoint);
-        if (result == TRUE)
-            return Boolean.TRUE;
-        return Boolean.FALSE;
+        return result == 1L;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/course/service/CourseService.java`
around lines 64 - 69, The code currently does a reference comparison against a
Long constant (TRUE) in isArrived; change the comparison to a value comparison:
retrieve the repository result as a Long (e.g., Long result =
courseRepository.isUserArrivedDestination(courseId, userPoint)) and return
Objects.equals(result, TRUE) or convert both to primitives and compare via
result.longValue() == TRUE.longValue(); update isArrived to use Objects.equals
or primitive equality so you compare values, not object references (referencing
isArrived and courseRepository.isUserArrivedDestination and the TRUE symbol).
src/main/java/com/econo_4factorial/newproject/travel/service/TravelService.java-97-105 (1)

97-105: ⚠️ Potential issue | 🟠 Major

진행 중 세션의 코스를 payload 값으로 다시 신뢰하면 상태가 섞입니다.

START에서 이미 코스를 고정했는데 Line 98, Line 113, Line 130, Line 143은 이후 이벤트마다 dto.courseId()를 다시 사용합니다. 중간에 다른 courseId가 들어오면 남은 시간/이탈/도착 판정이 저장된 TravelTrackingInfo.courseId와 다른 기준으로 계산됩니다.

예시 수정안
-        Long courseId = dto.courseId();
         TravelTrackingInfo info = travelTrackingInfoStore.getInfo(userId);
+        Long courseId = info.getCourseId();

pause, reStart, end도 같은 방식으로 현재 세션의 courseId를 사용하세요.

Also applies to: 111-119, 128-136, 141-150

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/travel/service/TravelService.java`
around lines 97 - 105, The code repeatedly trusts dto.courseId() for subsequent
events which can diverge from the session-stored course id and corrupt
TravelTrackingInfo; instead use the course id stored in the active session
(TravelTrackingInfo) — replace uses of dto.courseId() in this method and the
other event branches (where you obtain courseId from dto for pause, reStart,
end, etc.) with the session value (e.g., info.getCourseId() or
TravelTrackingInfo.courseId getter) after retrieving TravelTrackingInfo (info),
and guard against null info before using it so all travel calculations
(analyzeTravelStatus, remaining-time, leave/arrival logic) consistently use the
session’s course id.
src/main/java/com/econo_4factorial/newproject/travel/service/TravelService.java-82-82 (1)

82-82: ⚠️ Potential issue | 🟠 Major

산행 기록 시간을 클라이언트 payload에서 그대로 받으면 조작 가능합니다.

Line 82와 Line 144-145는 시작/종료 시각과 총 산행 시간을 모두 클라이언트가 보낸 값으로 채웁니다. 이 값들은 서버가 세션 생명주기에서 직접 계산할 수 있으므로, 지금처럼 두면 사용자가 임의의 기록 시간을 저장할 수 있습니다.

예시 수정안
-        LocalDateTime startedAt = TimeMapper.toLocalTime(dto.time());
+        LocalDateTime startedAt = LocalDateTime.now();
...
-        Duration totalTravelTime = Duration.ofMillis(dto.totalTravelTime());
-        LocalDateTime endAt = TimeMapper.toLocalTime(dto.time());
+        LocalDateTime endAt = LocalDateTime.now();
+        TravelTrackingInfo current = travelTrackingInfoStore.getInfo(userId);
+        Duration totalTravelTime = Duration.between(current.getStartedAt(), endAt);

테스트 가능성이 필요하면 Clock을 주입해서 서버 시각을 고정하세요.

Also applies to: 144-145

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/travel/service/TravelService.java`
at line 82, Client-supplied timestamps are used directly (e.g., LocalDateTime
startedAt = TimeMapper.toLocalTime(dto.time()) and the end/time fields set at
lines 144-145), allowing users to forge ride times; instead compute start, end
and duration on the server from server time (or from session lifecycle) and stop
using dto.time()/dto.endTime()/dto.totalTime() as authoritative values. Inject a
java.time.Clock into TravelService, derive server LocalDateTime.now(clock) for
start/end and calculate duration from those server times (or from persisted
session start/stop events) and replace usages of
TimeMapper.toLocalTime(dto.time()) and any assignments from dto for end/total
time with server-calculated values so only client payload metadata (not
authoritative timestamps) is accepted.
src/main/java/com/econo_4factorial/newproject/travel/service/TravelDomainService.java-31-32 (1)

31-32: ⚠️ Potential issue | 🟠 Major

누적 거리를 여기서 반올림하면 짧은 이동이 영구히 사라집니다.

Line 32는 누적 합계를 매 이벤트마다 0.01km 단위로 잘라서, 3~4m처럼 짧은 이동이 반복되면 총 이동거리가 계속 과소계상됩니다. 반올림은 저장값이 아니라 응답 직전에만 적용하는 편이 안전합니다.

예시 수정안
-        Double totalTravelDistance = Math.round((prevTotalTravelDistance + travelDistance) * 100) / 100.0 ; //소수점 둘 째 짜리까지 표기 km단위.
+        Double totalTravelDistance = prevTotalTravelDistance + travelDistance;

표시용 반올림은 DTO/매퍼에서만 적용하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/travel/service/TravelDomainService.java`
around lines 31 - 32, The code in TravelDomainService currently rounds the
running total each event (totalTravelDistance =
Math.round((prevTotalTravelDistance + travelDistance) * 100) / 100.0), which
loses small repeated movements; instead stop rounding when updating the
accumulator — compute totalTravelDistance as prevTotalTravelDistance +
travelDistance using the raw double from
travelDistanceCalculator.calculateDistanceFromLastLocation and persist/propagate
that precise value, and move any display rounding into the DTO/mapper or
response layer so only presentation uses Math.round.
src/main/java/com/econo_4factorial/newproject/travel/handler/WebSocketHandler.java-104-108 (1)

104-108: ⚠️ Potential issue | 🟠 Major

인증되지 않은 연결 종료 경로에서 null userId를 그대로 넘기고 있습니다.

인증 전에 끊긴 세션이면 getUserId()null입니다. Line 106-107처럼 그대로 cleanup에 넘기면 store 구현에 따라 종료 훅이 예외로 끝나고, 그 다음 Line 108 인증 맵 정리도 건너뛰게 됩니다.

예시 수정안
-        Long userId = webSocketAuthService.getUserId(session.getId());
-        travelService.deleteIfExistTravelTrackingInfo(userId);
-        webSocketAuthService.removeSession(session);
+        Long userId = webSocketAuthService.getUserId(session.getId());
+        try {
+            if (userId != null) {
+                travelService.deleteIfExistTravelTrackingInfo(userId);
+            }
+        } finally {
+            webSocketAuthService.removeSession(session);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/travel/handler/WebSocketHandler.java`
around lines 104 - 108, afterConnectionClosed currently retrieves userId via
webSocketAuthService.getUserId(session.getId()) and passes it directly to
travelService.deleteIfExistTravelTrackingInfo which can throw if userId is null;
change the logic in afterConnectionClosed to first remove the session id from
clients, then call webSocketAuthService.removeSession(session) unconditionally,
and only if the fetched userId is non-null invoke
travelService.deleteIfExistTravelTrackingInfo(userId); this ensures null userIds
are not passed to deleteIfExistTravelTrackingInfo and that removeSession always
runs (refer to afterConnectionClosed, getUserId,
travelService.deleteIfExistTravelTrackingInfo, and
webSocketAuthService.removeSession).
src/main/java/com/econo_4factorial/newproject/travel/service/TravelService.java-84-90 (1)

84-90: ⚠️ Potential issue | 🟠 Major

시작 상태를 검증 전에 저장하면 실패한 start 요청에도 추적 정보가 남습니다.

Line 84에서 TravelTrackingInfo를 먼저 만들고 있어서, 이후 Line 89-90에서 예외가 나면 실패 응답 이후에도 세션 상태가 남습니다. 코스 분석이 끝난 뒤에 저장하거나, 실패 시 반드시 정리해야 합니다.

예시 수정안
-        travelTrackingInfoStore.makeInfo(userId, courseId, startedAt, userPoint);
-
-        Point prevPoint = travelTrackingInfoStore.getLastPoint(userId);
-        Double totalTravelDistance = travelTrackingInfoStore.getTotalTravelDistance(userId);
-
-        TravelAnalysisResult result = travelDomainService.analyzeTravelStatus(courseId, prevPoint, userPoint, totalTravelDistance);
+        TravelAnalysisResult result = travelDomainService.analyzeTravelStatus(courseId, null, userPoint, 0.0);
+        travelTrackingInfoStore.makeInfo(userId, courseId, startedAt, userPoint);

makeInfo()를 먼저 유지해야 한다면, 실패 시 cleanup을 보장하는 try/finally가 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/travel/service/TravelService.java`
around lines 84 - 90, Currently travelTrackingInfoStore.makeInfo(userId,
courseId, startedAt, userPoint) is called before doing analysis, so if
travelDomainService.analyzeTravelStatus(...) throws the partially-created
tracking info remains; either move the makeInfo call to after the
analyzeTravelStatus + travelTrackingInfoStore.start(...) sequence, or wrap the
current flow in a try/finally: call makeInfo first, then run
analyzeTravelStatus(...) and start(...), and in the finally block remove/cleanup
the created TravelTrackingInfo when an exception occurred (use
travelTrackingInfoStore.delete/cleanup or a suitable method) so no stale session
remains; refer to travelTrackingInfoStore.makeInfo,
travelDomainService.analyzeTravelStatus, travelTrackingInfoStore.start to locate
the change.
🟡 Minor comments (9)
src/main/java/com/econo_4factorial/newproject/common/util/TimeMapper.java-12-16 (1)

12-16: ⚠️ Potential issue | 🟡 Minor

time 파라미터에 대한 null 검사가 필요합니다.

time이 null인 경우 Instant.ofEpochMilli(time) 호출 시 언박싱 과정에서 NullPointerException이 발생합니다. 방어적으로 null 체크를 추가하거나, null 허용 여부를 명시적으로 문서화하세요.

🛡️ null 검사 추가 제안
     public static LocalDateTime toLocalTime(Long time) {
+        if (time == null) {
+            return null;
+        }
         return Instant.ofEpochMilli(time)
                 .atZone(ZoneId.of("Asia/Seoul"))
                 .toLocalDateTime();
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/econo_4factorial/newproject/common/util/TimeMapper.java`
around lines 12 - 16, The toLocalTime method (TimeMapper.toLocalTime) currently
calls Instant.ofEpochMilli(time) without checking for a null Long, causing a
NullPointerException on unboxing; add a null check at the start of toLocalTime
and handle it defensively (either throw a clear IllegalArgumentException like
"time must not be null" or return null/Optional depending on project
convention), and update the method javadoc to document whether null is allowed
and what the method returns/throws; ensure references to ZoneId.of("Asia/Seoul")
and Instant.ofEpochMilli(time) remain unchanged except for the null guard.
src/main/java/com/econo_4factorial/newproject/travel/util/GeoUtil.java-13-17 (1)

13-17: ⚠️ Potential issue | 🟡 Minor

입력 배열 검증이 없어 ArrayIndexOutOfBoundsException 가능성.

coordinate 배열이 2개 미만의 요소를 가질 경우 런타임 예외가 발생합니다. 방어적 검증을 추가하세요.

🛡️ 수정 제안
 public static Point toPoint(double[] coordinate) {
+    if (coordinate == null || coordinate.length < 2) {
+        throw new IllegalArgumentException("Coordinate must have at least 2 elements");
+    }
     Point point = geometryFactory.createPoint(new Coordinate(coordinate[0], coordinate[1]));
     point.setSRID(WGS84);
     return point;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/econo_4factorial/newproject/travel/util/GeoUtil.java`
around lines 13 - 17, The toPoint method can throw
ArrayIndexOutOfBoundsException when coordinate is null or has fewer than 2
elements; add defensive validation at the start of toPoint to check that
coordinate != null and coordinate.length >= 2 (and optionally that
coordinate[0]/[1] are finite), and if not, throw an IllegalArgumentException
with a clear message instead of proceeding to call
geometryFactory.createPoint(...); keep the rest of the logic (createPoint and
setSRID(WGS84)) unchanged.
src/main/java/com/econo_4factorial/newproject/common/exception/WebSocketIOException.java-3-6 (1)

3-6: ⚠️ Potential issue | 🟡 Minor

오류 유형 코드 명명 규칙과 HTTP 상태 코드의 불일치.

WEB_SOCKET_IO_EXCEPTION의 오류 코드가 "WEB_SOCKET500"으로 지정되어 있으나, HttpStatus.BAD_REQUEST(400)를 사용합니다. 코드베이스의 명명 규칙을 보면 "500" 접두사는 INTERNAL_SERVER_ERROR와 대응되고(예: AUTH500_001INTERNAL_SERVER_ERROR), "400" 접두사는 BAD_REQUEST와 대응됩니다. 또한 InternalServerException을 상속하는 것도 5xx 상태 코드를 암시합니다.

HttpStatus.INTERNAL_SERVER_ERROR로 변경하거나, 의도적으로 클라이언트 오류라면 오류 코드를 "WEB_SOCKET400"으로 수정하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/common/exception/WebSocketIOException.java`
around lines 3 - 6, The error code and HTTP status are inconsistent:
WebSocketIOException currently extends InternalServerException and uses
CommonErrorType.WEB_SOCKET_IO_EXCEPTION (which is named/numbered as a
500-series) but the underlying error mapping uses HttpStatus.BAD_REQUEST; either
change the exception to return HttpStatus.INTERNAL_SERVER_ERROR or rename the
error code to a 400-series. Fix by updating WebSocketIOException (or its
base/constructor usage) so it aligns with
CommonErrorType.WEB_SOCKET_IO_EXCEPTION by using
HttpStatus.INTERNAL_SERVER_ERROR when instantiating/propagating the error, or
alternatively rename CommonErrorType.WEB_SOCKET_IO_EXCEPTION to a 400-series
code (e.g., WEB_SOCKET400) and change the exception to extend a BadRequest-type
exception class; reference WebSocketIOException, InternalServerException, and
CommonErrorType.WEB_SOCKET_IO_EXCEPTION to locate the changes.
src/main/java/com/econo_4factorial/newproject/travel/exception/TravelErrorType.java-10-13 (1)

10-13: ⚠️ Potential issue | 🟡 Minor

중복된 에러 코드를 수정하세요.

Line 10과 Line 12가 모두 TRAVEL404_001을 사용하고 있어, 클라이언트가 에러 코드를 기준으로 두 오류를 구분할 수 없습니다. 고유한 에러 코드로 분리해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/travel/exception/TravelErrorType.java`
around lines 10 - 13, In the TravelErrorType enum update duplicate error codes
so each constant has a unique identifier: change the code value for
NOT_EXIST_EVENT_EXCEPTION (currently same as TRAVEL_RECORD_NOT_FOUND_EXCEPTION)
to a distinct code (e.g., TRAVEL404_002 or another unused TRAVEL404_x), and
ensure TRAVEL_TRACKING_INFO_NOT_FOUND_EXCEPTION’s code remains unique; modify
the string passed to NOT_EXIST_EVENT_EXCEPTION in the enum declaration (refer to
TravelErrorType, TRAVEL_RECORD_NOT_FOUND_EXCEPTION, NOT_EXIST_EVENT_EXCEPTION,
TRAVEL_TRACKING_INFO_NOT_FOUND_EXCEPTION) so clients can distinguish errors by
code.
src/main/java/com/econo_4factorial/newproject/course/exception/CourseErrorType.java-11-11 (1)

11-11: ⚠️ Potential issue | 🟡 Minor

에러 코드 네임스페이스가 enum 소속과 불일치합니다.

Line [11]의 TRAVEL500_001CourseErrorType 내부 코드 체계와 어긋나서 로그 집계/클라이언트 분기 시 혼선을 만들 수 있습니다. COURSE... 계열로 맞추는 편이 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/course/exception/CourseErrorType.java`
at line 11, The enum constant CLOSEST_COORDINATE_NOT_FOUND_EXCEPTION in
CourseErrorType uses the wrong namespace code "TRAVEL500_001"; change that code
string to the COURSE namespace (e.g. "COURSE500_001") so it aligns with
CourseErrorType, update any callers/tests/logging that assert the old code to
the new value, and ensure the enum constructor invocation for
CLOSEST_COORDINATE_NOT_FOUND_EXCEPTION is the only site changed (keep the
existing HttpStatus.INTERNAL_SERVER_ERROR and message unchanged).
src/main/java/com/econo_4factorial/newproject/pathway/domain/Pathway.java-27-28 (1)

27-28: ⚠️ Potential issue | 🟡 Minor

GeoUtil.toLineString() 메서드에서 명시적 SRID 설정이 누락되었습니다.

src/main/java/com/econo_4factorial/newproject/travel/util/GeoUtil.javatoLineString() 메서드(Line 25-31)는 LineString을 반환할 때 명시적인 SRID 설정이 없습니다. 같은 클래스의 toPoint() 메서드들은 point.setSRID(WGS84);로 명시적으로 SRID를 설정하는 반면, toLineString()은 GeometryFactory의 SRID만 의존합니다. 일관성과 안정성을 위해 다음과 같이 수정하세요:

public static LineString toLineString(List<Point> points) {
    Coordinate[] coordinates = points.stream()
            .map(Point::getCoordinate)
            .toArray(Coordinate[]::new);
    LineString lineString = geometryFactory.createLineString(coordinates);
    lineString.setSRID(WGS84);
    return lineString;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/econo_4factorial/newproject/pathway/domain/Pathway.java`
around lines 27 - 28, The toLineString() method in GeoUtil is missing an
explicit SRID assignment; update GeoUtil.toLineString(List<Point> points) to set
the SRID on the created LineString (use the same WGS84 constant used by
toPoint()) after creating it with geometryFactory.createLineString(...) by
calling lineString.setSRID(WGS84) before returning so the coordinates field
(Pathway.coordinates) will have the correct SRID.
src/main/java/com/econo_4factorial/newproject/travel/dto/res/GetTravelRecordDetailRes.java-29-29 (1)

29-29: ⚠️ Potential issue | 🟡 Minor

하드코딩된 mountainId 수정 필요

mountainId1로 하드코딩되어 있어 무등산 외 다른 산의 데이터 처리 시 문제가 발생합니다. TravelRecordDetailDTO에서 mountainId를 전달받도록 수정이 필요합니다.

이 작업을 추적할 이슈를 생성해 드릴까요?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/travel/dto/res/GetTravelRecordDetailRes.java`
at line 29, The mountainId is hardcoded to 1 in GetTravelRecordDetailRes; update
the constructor or mapping code that builds GetTravelRecordDetailRes to accept
and set mountainId from the incoming TravelRecordDetailDTO (or the DTO used)
instead of the literal 1. Locate where GetTravelRecordDetailRes is constructed
(e.g., its constructor or a static from/to method) and replace the hardcoded
value with travelRecordDetailDTO.getMountainId() (or the appropriate getter) and
ensure the field and any builder use that value so other mountains are handled
correctly.
src/main/java/com/econo_4factorial/newproject/travel/controller/TravelRecordController.java-31-31 (1)

31-31: ⚠️ Potential issue | 🟡 Minor

메서드 이름 중복 및 부적절한 명명

두 GET 메서드가 모두 getPathwaysOfCourse로 명명되어 있습니다. 이 이름은 Pathway 조회를 암시하지만, 실제로는 TravelRecord를 조회합니다. 각 메서드의 목적에 맞는 이름으로 변경해야 합니다.

🐛 제안하는 수정
-    public ApiResult<ApiResult.SuccessBody<GetTravelRecordByMonthRes>> getPathwaysOfCourse(
+    public ApiResult<ApiResult.SuccessBody<GetTravelRecordByMonthRes>> getTravelRecordsByMonth(
             `@Parameter`(name = "userId", description = "사용자 ID", required = true)
             `@UserId` Long userId,
-    public ApiResult<ApiResult.SuccessBody<GetTravelRecordDetailRes>> getPathwaysOfCourse(
+    public ApiResult<ApiResult.SuccessBody<GetTravelRecordDetailRes>> getTravelRecordDetail(
             `@Parameter`(name = "userId", description = "사용자 ID", required = true)
             `@UserId` Long userId,

Also applies to: 48-48

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/travel/controller/TravelRecordController.java`
at line 31, Two GET methods in TravelRecordController are both named
getPathwaysOfCourse which is misleading and duplicates a name intended for
Pathway logic; rename them to reflect they return TravelRecord data (e.g.,
rename getPathwaysOfCourse that returns GetTravelRecordByMonthRes to
getTravelRecordsByMonth and rename the other getPathwaysOfCourse to a
descriptive name like getTravelRecordById or getTravelRecordsByCourse as
appropriate). Update all references/usages, method signatures, any mapping
annotations, tests, and documentation comments to use the new names so
signatures and routing remain consistent in class TravelRecordController.
src/main/java/com/econo_4factorial/newproject/common/util/webSocket/WebSocketResponser.java-34-35 (1)

34-35: ⚠️ Potential issue | 🟡 Minor

로그 메시지 오류

실패 응답을 전송했는데 로그 메시지가 "웹소켓 메세지 전송 성공"으로 되어 있어 혼란을 줄 수 있습니다. 실패 응답 전송임을 명확히 하는 것이 좋습니다.

🐛 제안하는 수정
         session.sendMessage(new TextMessage(failResponseJson));
-        log.info("웹소켓 메세지 전송 성공 : session = {}. failResponse = {}", session.getId(), failResponseJson);
+        log.info("웹소켓 실패 응답 전송 : session = {}. failResponse = {}", session.getId(), failResponseJson);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/common/util/webSocket/WebSocketResponser.java`
around lines 34 - 35, The log message after sending the failure payload is
misleading; update the WebSocketResponser code that calls
session.sendMessage(new TextMessage(failResponseJson)) and currently logs via
log.info("웹소켓 메세지 전송 성공 : session = {}. failResponse = {}", session.getId(),
failResponseJson) to clearly indicate a failure send (e.g., change the message
text to indicate failure and use an appropriate level like warn/error). Locate
the logging call in the WebSocketResponser class and replace the success text
with a failure-specific message referencing session.getId() and
failResponseJson, and adjust the log level to reflect an error condition.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dafaa821-0840-49b4-8b95-ec9604ed20a4

📥 Commits

Reviewing files that changed from the base of the PR and between fa260b5 and 7b8023f.

📒 Files selected for processing (79)
  • .github/workflows/trigger-cicd.yml
  • build.gradle
  • src/main/java/com/econo_4factorial/newproject/auth/controller/OAuthController.java
  • src/main/java/com/econo_4factorial/newproject/auth/jwt/service/AuthTokenService.java
  • src/main/java/com/econo_4factorial/newproject/base/domain/Base.java
  • src/main/java/com/econo_4factorial/newproject/base/dto/BaseDTO.java
  • src/main/java/com/econo_4factorial/newproject/base/service/weather/WeatherService.java
  • src/main/java/com/econo_4factorial/newproject/common/annotation/resolver/UserIdResolver.java
  • src/main/java/com/econo_4factorial/newproject/common/config/GeometryConfig.java
  • src/main/java/com/econo_4factorial/newproject/common/config/WebSocketConfig.java
  • src/main/java/com/econo_4factorial/newproject/common/exception/CommonErrorType.java
  • src/main/java/com/econo_4factorial/newproject/common/exception/WebSocketIOException.java
  • src/main/java/com/econo_4factorial/newproject/common/util/DateUtil.java
  • src/main/java/com/econo_4factorial/newproject/common/util/TimeMapper.java
  • src/main/java/com/econo_4factorial/newproject/common/util/webSocket/WebSocketAuthService.java
  • src/main/java/com/econo_4factorial/newproject/common/util/webSocket/WebSocketFailRes.java
  • src/main/java/com/econo_4factorial/newproject/common/util/webSocket/WebSocketResponser.java
  • src/main/java/com/econo_4factorial/newproject/common/util/webSocket/WebSocketSuccessRes.java
  • src/main/java/com/econo_4factorial/newproject/course/domain/Course.java
  • src/main/java/com/econo_4factorial/newproject/course/dto/ClosestCoordinateInfo.java
  • src/main/java/com/econo_4factorial/newproject/course/exception/CourseErrorType.java
  • src/main/java/com/econo_4factorial/newproject/course/exception/InternalServerException/ClosestCoordinateNotFoundException.java
  • src/main/java/com/econo_4factorial/newproject/course/repository/CourseRepository.java
  • src/main/java/com/econo_4factorial/newproject/course/service/CourseLocationMatcher.java
  • src/main/java/com/econo_4factorial/newproject/course/service/CourseService.java
  • src/main/java/com/econo_4factorial/newproject/pathway/domain/Pathway.java
  • src/main/java/com/econo_4factorial/newproject/pathway/dto/PathwayDTO.java
  • src/main/java/com/econo_4factorial/newproject/pathway/dto/res/GetPathwaysOfCourseRes.java
  • src/main/java/com/econo_4factorial/newproject/pathway/dto/res/PathwayCoordinatesArrayDTO.java
  • src/main/java/com/econo_4factorial/newproject/pathway/mapper/PathwayMapper.java
  • src/main/java/com/econo_4factorial/newproject/pathway/service/CoursePathwaySequenceService.java
  • src/main/java/com/econo_4factorial/newproject/pathway/service/PathwayService.java
  • src/main/java/com/econo_4factorial/newproject/travel/Status.java
  • src/main/java/com/econo_4factorial/newproject/travel/TravelEvent.java
  • src/main/java/com/econo_4factorial/newproject/travel/controller/TravelRecordController.java
  • src/main/java/com/econo_4factorial/newproject/travel/domain/TravelRecord.java
  • src/main/java/com/econo_4factorial/newproject/travel/domain/TravelTrackingInfo.java
  • src/main/java/com/econo_4factorial/newproject/travel/domain/vo/RemainingTime.java
  • src/main/java/com/econo_4factorial/newproject/travel/dto/Payload.java
  • src/main/java/com/econo_4factorial/newproject/travel/dto/TravelAnalysisInput.java
  • src/main/java/com/econo_4factorial/newproject/travel/dto/TravelAnalysisResult.java
  • src/main/java/com/econo_4factorial/newproject/travel/dto/TravelRecordDTO.java
  • src/main/java/com/econo_4factorial/newproject/travel/dto/TravelRecordDetailDTO.java
  • src/main/java/com/econo_4factorial/newproject/travel/dto/req/CurrentPositionEventReq.java
  • src/main/java/com/econo_4factorial/newproject/travel/dto/req/EndEventReq.java
  • src/main/java/com/econo_4factorial/newproject/travel/dto/req/KeepAliveEventReq.java
  • src/main/java/com/econo_4factorial/newproject/travel/dto/req/PauseEventReq.java
  • src/main/java/com/econo_4factorial/newproject/travel/dto/req/RestartEventReq.java
  • src/main/java/com/econo_4factorial/newproject/travel/dto/req/StartEventReq.java
  • src/main/java/com/econo_4factorial/newproject/travel/dto/res/CurrentPositionEventRes.java
  • src/main/java/com/econo_4factorial/newproject/travel/dto/res/EndEventRes.java
  • src/main/java/com/econo_4factorial/newproject/travel/dto/res/GetTravelRecordByMonthRes.java
  • src/main/java/com/econo_4factorial/newproject/travel/dto/res/GetTravelRecordDetailRes.java
  • src/main/java/com/econo_4factorial/newproject/travel/dto/res/PauseEventRes.java
  • src/main/java/com/econo_4factorial/newproject/travel/dto/res/RestartEventRes.java
  • src/main/java/com/econo_4factorial/newproject/travel/dto/res/StartEventRes.java
  • src/main/java/com/econo_4factorial/newproject/travel/dto/res/TravelEventResponse.java
  • src/main/java/com/econo_4factorial/newproject/travel/dto/res/TravelEventResponseData.java
  • src/main/java/com/econo_4factorial/newproject/travel/exception/NotAllowedEventForStatusException.java
  • src/main/java/com/econo_4factorial/newproject/travel/exception/NotExistEventException.java
  • src/main/java/com/econo_4factorial/newproject/travel/exception/TravelErrorType.java
  • src/main/java/com/econo_4factorial/newproject/travel/exception/TravelRecordNotFoundException.java
  • src/main/java/com/econo_4factorial/newproject/travel/exception/TravelTrackingInfoNotFoundException.java
  • src/main/java/com/econo_4factorial/newproject/travel/exception/UnauthenticatedException.java
  • src/main/java/com/econo_4factorial/newproject/travel/handler/WebSocketHandler.java
  • src/main/java/com/econo_4factorial/newproject/travel/mapper/TravelMapper.java
  • src/main/java/com/econo_4factorial/newproject/travel/repository/TravelRecordRepository.java
  • src/main/java/com/econo_4factorial/newproject/travel/service/RemainingTimeCalculator.java
  • src/main/java/com/econo_4factorial/newproject/travel/service/TravelDistanceCalculator.java
  • src/main/java/com/econo_4factorial/newproject/travel/service/TravelDomainService.java
  • src/main/java/com/econo_4factorial/newproject/travel/service/TravelRecordService.java
  • src/main/java/com/econo_4factorial/newproject/travel/service/TravelService.java
  • src/main/java/com/econo_4factorial/newproject/travel/service/TravelTrackingInfoStore.java
  • src/main/java/com/econo_4factorial/newproject/travel/util/EventPolicy.java
  • src/main/java/com/econo_4factorial/newproject/travel/util/GeoUtil.java
  • src/main/java/com/econo_4factorial/newproject/travel/util/PayloadMapper.java
  • src/main/java/com/econo_4factorial/newproject/travel/util/TravelResponseMapper.java
  • src/main/resources/application-jpa.yml
  • src/main/resources/data.sql

package com.econo_4factorial.newproject.course.repository;

import com.econo_4factorial.newproject.course.domain.Course;
import feign.Param;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: 프로젝트 내 다른 Repository에서 사용하는 `@Param` import 확인

rg -n "import.*Param" --glob '*Repository.java'

Repository: JNU-econovation/sangyeol-backend

Length of output: 183


🏁 Script executed:

# Read the CourseRepository.java file to examine imports, `@Param` usage, and queries
head -100 src/main/java/com/econo_4factorial/newproject/course/repository/CourseRepository.java

Repository: JNU-econovation/sangyeol-backend

Length of output: 2341


🏁 Script executed:

# Find all Repository files in the project to check import patterns
find . -type f -name "*Repository.java" -not -path "*/target/*"

Repository: JNU-econovation/sangyeol-backend

Length of output: 1463


🏁 Script executed:

# Check what Spring Data dependencies are declared
find . -name "pom.xml" -o -name "build.gradle*" | head -5

Repository: JNU-econovation/sangyeol-backend

Length of output: 90


🏁 Script executed:

# Check imports in other Repository files to see the correct pattern
rg -A 10 "^import" src/main/java/com/econo_4factorial/newproject/base/repository/BaseRepository.java | head -30

Repository: JNU-econovation/sangyeol-backend

Length of output: 339


🏁 Script executed:

# Check for `@Query` and `@Param` usage in another repository
rg -B 2 -A 5 "@Query" src/main/java/com/econo_4factorial/newproject/user/repository/UserRepository.java | head -30

Repository: JNU-econovation/sangyeol-backend

Length of output: 58


🏁 Script executed:

# Examine build.gradle for Spring Data and Feign dependencies
cat build.gradle

Repository: JNU-econovation/sangyeol-backend

Length of output: 2860


🏁 Script executed:

# Check if there are any other `@Query` methods with `@Param` to see if this is a pattern issue
rg -B 5 -A 10 "@Query.*nativeQuery" --max-count 5

Repository: JNU-econovation/sangyeol-backend

Length of output: 58


🏁 Script executed:

# Search for correct Param import pattern in the entire codebase
rg "org.springframework.data.repository.query.Param" --type java

Repository: JNU-econovation/sangyeol-backend

Length of output: 58


🏁 Script executed:

# Check if feign.Param is used anywhere else in repository methods
rg "@Param" src/main/java/com/econo_4factorial/newproject --type java | head -20

Repository: JNU-econovation/sangyeol-backend

Length of output: 3012


feign.Param import으로 인해 런타임 오류가 발생합니다.

feign.Param은 OpenFeign HTTP 클라이언트용 어노테이션입니다. Spring Data JPA의 @Query 어노테이션에서는 org.springframework.data.repository.query.Param을 사용해야 합니다. 현재 코드는 Spring Data JPA가 파라미터를 인식하지 못하여 쿼리 실행 시 런타임 예외가 발생합니다.

수정 제안
-import feign.Param;
+import org.springframework.data.repository.query.Param;

추가로, isUserArrivedDestination() 메서드의 반환 타입을 검토하세요. 쿼리는 boolean 표현식(ST_Distance_Sphere(...) <= 10)을 반환하므로, 메서드 반환 타입을 boolean으로 변경하는 것이 의미론적으로 더 정확합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import feign.Param;
import org.springframework.data.repository.query.Param;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/course/repository/CourseRepository.java`
at line 4, 현재 CourseRepository의 잘못된 import feign.Param을
org.springframework.data.repository.query.Param으로 교체하고, `@Query에서` 사용하는 파라미터
어노테이션이 org.springframework.data.repository.query.Param을 참조하도록 수정하세요; 또한 쿼리
표현식(ST_Distance_Sphere(...) <= 10)을 반영하여 isUserArrivedDestination 메서드의 반환 타입을
Boolean/boolean에서 boolean으로 변경하고 메서드 시그니처 내 파라미터들(예: 메서드의 `@Param`("userLat"),
`@Param`("userLng") 등)에 올바른 `@Param` 애노테이션을 사용하도록 업데이트하세요.

Comment on lines +46 to +58
@GetMapping("/{recordId}/details")
@Operation(summary = "산행 기록 상세조회", description = "특정 산행 기록을 상세조회 합니다")
public ApiResult<ApiResult.SuccessBody<GetTravelRecordDetailRes>> getPathwaysOfCourse(
@Parameter(name = "userId", description = "사용자 ID", required = true)
@UserId Long userId,

@Parameter(name = "recordId", description = "산행 기록 ID", required = true)
@PathVariable Long recordId
) {
TravelRecordDetailDTO recordDetail = travelRecordService.findRecordById(recordId);
log.info("산행 기록 상세 조회 요청. userId = {}. recordId= {}. 조회된 산행 상세기록 = {}.", userId, recordId, recordDetail);
return ApiResponse.success(GetTravelRecordDetailRes.from(recordDetail), HttpStatus.OK);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: TravelRecordService의 findRecordById 구현을 확인합니다.

ast-grep --pattern $'public $_ findRecordById($_) {
  $$$
}'

Repository: JNU-econovation/sangyeol-backend

Length of output: 58


🏁 Script executed:

# Find TravelRecordService file
fd -t f "TravelRecordService" --extension java

Repository: JNU-econovation/sangyeol-backend

Length of output: 161


🏁 Script executed:

# Search for findRecordById method implementation
rg "findRecordById" -t java -B 2 -A 10

Repository: JNU-econovation/sangyeol-backend

Length of output: 3447


🏁 Script executed:

# Check for any authorization/permission checks in TravelRecordController
rg -t java "Authorization|Permission|Security|@Secured|@PreAuthorize" src/main/java/com/econo_4factorial/newproject/travel/controller/TravelRecordController.java

Repository: JNU-econovation/sangyeol-backend

Length of output: 58


🏁 Script executed:

# Get the TravelRecord entity structure
fd -t f "TravelRecord" --extension java src/main/java/com/econo_4factorial/newproject/travel/

Repository: JNU-econovation/sangyeol-backend

Length of output: 866


🏁 Script executed:

# Check TravelRecord entity and repository
rg "class TravelRecord" -t java -A 20

Repository: JNU-econovation/sangyeol-backend

Length of output: 8817


🏁 Script executed:

# Check if TravelRecordRepository has any ownership verification methods
rg "findById\|findBy.*userId" -t java src/main/java/com/econo_4factorial/newproject/travel/

Repository: JNU-econovation/sangyeol-backend

Length of output: 58


🏁 Script executed:

# Check TravelRecordController class annotations and other methods
cat -n src/main/java/com/econo_4factorial/newproject/travel/controller/TravelRecordController.java | head -70

Repository: JNU-econovation/sangyeol-backend

Length of output: 3980


소유권 검증 없이 임의의 기록에 접근 가능한 보안 취약점

findRecordByIddeleteRecordById 메서드가 userId를 사용하지 않아, 다른 사용자의 산행 기록을 조회하고 삭제할 수 있습니다. findRecordByMonth에서는 올바르게 userId로 필터링하는 것과 달리, 상세조회와 삭제 메서드는 recordId만으로 소유권 검증 없이 데이터에 접근합니다.

TravelRecordServicefindRecordById(Long recordId)deleteRecordById(Long recordId) 메서드를 userId 파라미터를 받도록 수정하고, 조회/삭제 전에 해당 기록이 요청한 사용자의 소유임을 검증해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/travel/controller/TravelRecordController.java`
around lines 46 - 58, The getPathwaysOfCourse controller calls
TravelRecordService.findRecordById(recordId) without verifying ownership; update
both TravelRecordService.findRecordById and deleteRecordById to accept the
requesting userId (e.g., findRecordById(Long userId, Long recordId) and
deleteRecordById(Long userId, Long recordId)), implement ownership check inside
those service methods (throw or return not-found/forbidden when record.userId !=
userId), and change TravelRecordController.getPathwaysOfCourse to call the new
signature with the `@UserId` Long userId so detail retrieval and deletion are
filtered by the requesting user (use findRecordByMonth as the reference for
filtering logic).

Comment on lines +60 to +72
@DeleteMapping("/{recordId}")
@Operation(summary = "산행 기록 삭제", description = "특정 산행 기록을 삭제합니다")
public ApiResult<ApiResult.SuccessBody<Void>> deleteRecordById(
@Parameter(name = "userId", description = "사용자 ID", required = true)
@UserId Long userId,

@Parameter(name = "recordId", description = "산행 기록 ID", required = true)
@PathVariable Long recordId
) {
travelRecordService.deleteRecordById(recordId);
log.info("산행 기록 삭제 요청. userId = {}. recordId= {}. ", userId, recordId);
return ApiResponse.success(HttpStatus.OK);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

소유권 검증 누락 - 보안 취약점

deleteRecordById에서 userId를 추출하지만 실제 삭제 시 해당 사용자가 레코드의 소유자인지 검증하지 않습니다. 이로 인해 인증된 모든 사용자가 다른 사용자의 산행 기록을 삭제할 수 있는 권한 상승(Privilege Escalation) 취약점이 발생합니다.

🔒 제안하는 수정
     public ApiResult<ApiResult.SuccessBody<Void>> deleteRecordById(
             `@Parameter`(name = "userId", description = "사용자 ID", required = true)
             `@UserId` Long userId,

             `@Parameter`(name = "recordId", description = "산행 기록 ID", required = true)
             `@PathVariable` Long recordId
     ) {
-        travelRecordService.deleteRecordById(recordId);
+        travelRecordService.deleteRecordById(userId, recordId);
         log.info("산행 기록 삭제 요청. userId = {}. recordId= {}. ", userId, recordId);
         return ApiResponse.success(HttpStatus.OK);
     }

서비스 레이어에서 userId를 검증하여 레코드 소유자만 삭제할 수 있도록 구현해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/travel/controller/TravelRecordController.java`
around lines 60 - 72, The controller currently calls
travelRecordService.deleteRecordById(recordId) without verifying ownership;
update the API and service flow so ownership is checked: change the controller
to pass the authenticated userId into the service (e.g., call
travelRecordService.deleteRecordById(userId, recordId)) and implement ownership
validation inside TravelRecordService.deleteRecordById(Long userId, Long
recordId) so it fetches the record, confirms record.getOwnerId() (or equivalent)
matches userId, throws an appropriate exception (e.g., AccessDeniedException or
custom) if not, and only then deletes the record.

Comment on lines +28 to +29
@Column(columnDefinition = "LINESTRING SRID 4326", nullable = false)
private LineString paths;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Hibernate Spatial 의존성 확인

echo "=== Checking for hibernate-spatial dependency ==="
fd -e gradle -e xml | xargs grep -l "hibernate-spatial\|jts-core" 2>/dev/null | head -5 | xargs cat 2>/dev/null | grep -i "hibernate-spatial\|jts\|spatial"

Repository: JNU-econovation/sangyeol-backend

Length of output: 252


🏁 Script executed:

# Check for Hibernate dialect configuration
fd -type f \( -name "*.properties" -o -name "*.yml" -o -name "*.yaml" \) | xargs grep -l "hibernate\|dialect\|spatial" 2>/dev/null | head -5

Repository: JNU-econovation/sangyeol-backend

Length of output: 246


🏁 Script executed:

# Check the build.gradle file to see full dependency context
fd -name "build.gradle*" -o -name "pom.xml" | xargs cat 2>/dev/null | grep -A 50 -B 5 "hibernate-spatial\|spatial"

Repository: JNU-econovation/sangyeol-backend

Length of output: 306


🏁 Script executed:

# Verify TravelRecord.java imports
cat -n src/main/java/com/econo_4factorial/newproject/travel/domain/TravelRecord.java | head -30

Repository: JNU-econovation/sangyeol-backend

Length of output: 1133


🏁 Script executed:

# Find and check application properties/yml files
fd "application" | xargs cat 2>/dev/null | head -100

Repository: JNU-econovation/sangyeol-backend

Length of output: 2316


🏁 Script executed:

# Check build.gradle for complete dependency block
fd "build.gradle" | xargs cat 2>/dev/null

Repository: JNU-econovation/sangyeol-backend

Length of output: 2860


Hibernate Spatial 방언(Dialect) 설정을 공간 데이터 지원에 맞게 업데이트하세요.

hibernate-spatial:6.5.2.Finaljts-core:1.19.0 의존성은 이미 포함되어 있으나, 현재 org.hibernate.dialect.MySQL8Dialect는 공간 데이터 타입을 제대로 처리하지 못합니다. MySQL에서 LINESTRING과 같은 공간 데이터를 올바르게 지원하려면 application.yml의 Hibernate 방언을 org.hibernate.spatial.dialect.mysql.MySQL8SpatialDialect로 변경해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/travel/domain/TravelRecord.java`
around lines 28 - 29, The Hibernate dialect is incorrect for spatial types used
by the TravelRecord entity (the paths field of type LineString); update your
Hibernate dialect setting in application.yml to use the spatial-aware dialect
(set the hibernate.dialect property to
org.hibernate.spatial.dialect.mysql.MySQL8SpatialDialect) so MySQL LINESTRING
and other spatial types are handled correctly at runtime.

Comment on lines +47 to +55
public TravelRecordDetailDTO findRecordById(Long recordId) {
return travelRecordRepository.findById(recordId)
.map(TravelRecordDetailDTO::from)
.orElseThrow(TravelRecordNotFoundException::new);
}

@Transactional
public void deleteRecordById(Long recordId) {
travelRecordRepository.deleteById(recordId);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

상세 조회/삭제에 소유권 검증을 강제할 수 없습니다.

Line 47-55는 recordId만 받아서 서비스 레이어에서 소유권 검증을 강제할 수 없습니다. 이 시그니처를 그대로 두면 상위 계층이 실수했을 때 다른 사용자의 산행 기록을 열람/삭제하는 경로가 열립니다.

예시 수정안
-    public TravelRecordDetailDTO findRecordById(Long recordId) {
-        return travelRecordRepository.findById(recordId)
+    public TravelRecordDetailDTO findRecordById(Long userId, Long recordId) {
+        return travelRecordRepository.findByIdAndUserId(recordId, userId)
                 .map(TravelRecordDetailDTO::from)
                 .orElseThrow(TravelRecordNotFoundException::new);
     }

     `@Transactional`
-    public void deleteRecordById(Long recordId) {
-        travelRecordRepository.deleteById(recordId);
+    public void deleteRecordById(Long userId, Long recordId) {
+        travelRecordRepository.deleteByIdAndUserId(recordId, userId);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/travel/service/TravelRecordService.java`
around lines 47 - 55, The service methods findRecordById and deleteRecordById
currently take only recordId so ownership cannot be enforced; change their
signatures to accept the requesting principal (e.g., requestingMemberId or
Member object) and inside TravelRecordService check that the fetched
TravelRecord's ownerId matches the requester before returning or deleting (if
mismatch, throw TravelRecordNotFoundException or an AccessDenied exception);
update any callers to pass the authenticated user's id and ensure
travelRecordRepository.findById(...) is used to both verify ownership and then
perform the read or delete operation (use the same record lookup for both checks
to avoid TOCTOU).

@Slf4j
@Component
public class TravelTrackingInfoStore {
private final Map<Long, TravelTrackingInfo> infoStore = new HashMap<>();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

HashMap 사용은 웹소켓 동시 처리에서 안전하지 않습니다.

Line 21의 HashMap@Component 싱글턴에서 동시 접근 시 레이스/가시성 문제를 유발할 수 있습니다. 실시간 위치 업데이트 저장소는 thread-safe 컬렉션으로 바꿔야 합니다.

제안 diff
-import java.util.HashMap;
 import java.util.Map;
 import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
@@
-    private final Map<Long, TravelTrackingInfo> infoStore = new HashMap<>();
+    private final Map<Long, TravelTrackingInfo> infoStore = new ConcurrentHashMap<>();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/travel/service/TravelTrackingInfoStore.java`
at line 21, The field infoStore in the TravelTrackingInfoStore singleton is
currently a non-thread-safe HashMap and must be replaced with a thread-safe
collection to avoid race/visibility issues during concurrent WebSocket updates;
change the declaration and initialization of infoStore from HashMap to a
thread-safe implementation such as java.util.concurrent.ConcurrentHashMap (or
use ConcurrentMap<Long, TravelTrackingInfo> infoStore = new
ConcurrentHashMap<>()) and update imports/usages in the TravelTrackingInfoStore
class accordingly so all concurrent reads/writes are safe.

Comment on lines +36 to +63
public void start(Long userId, RemainingTime remainingTime) {
TravelTrackingInfo info = infoStore.get(userId);
info.start(remainingTime);
log.info("산행 start. userId : {}", userId);
}

public void currentPosition(Long userId, Point userPoint, RemainingTime remainingTime, Double totalTravelDistance) {
TravelTrackingInfo info = infoStore.get(userId);
info.currentPosition(userPoint, totalTravelDistance, remainingTime);
log.info("산행 정보 업데이트. userId : {}", userId);
}

public void pause(Long userId, Point userPoint, RemainingTime remainingTime, Double totalTravelDistance) {
TravelTrackingInfo info = infoStore.get(userId);
info.pause(userPoint, totalTravelDistance, remainingTime);
log.info("산행 중지. userId : {}", userId);
}

public void reStart(Long userId, Point userPoint, RemainingTime remainingTime, Double totalTravelDistance) {
TravelTrackingInfo info = infoStore.get(userId);
info.reStart(userPoint, totalTravelDistance, remainingTime);
log.info("산행 재개. userId : {}", userId);
}

public TravelTrackingInfo end(LocalDateTime endAt, Long userId, Point userPoint, RemainingTime remainingTime, Double totalTravelDistance, Duration totalTravelTime) {
TravelTrackingInfo info = infoStore.get(userId);
info.end(endAt, userPoint, totalTravelDistance, remainingTime, totalTravelTime);
log.info("산행 종료. userId : {}", userId);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

저장 정보 미존재 시 NullPointerException이 발생합니다.

Line 37, 43, 49, 55, 61에서 infoStore.get(userId) 결과를 검증하지 않고 바로 호출합니다. 이벤트 순서가 어긋나거나 세션 정리 후 재요청되면 NPE로 터집니다. 조회 메서드들처럼 도메인 예외(TravelTrackingInfoNotFoundException)로 일관 처리하세요.

제안 diff
     public void start(Long userId, RemainingTime remainingTime) {
-        TravelTrackingInfo info = infoStore.get(userId);
+        TravelTrackingInfo info = getInfoOrThrow(userId);
         info.start(remainingTime);
         log.info("산행 start. userId : {}", userId);
     }

     public void currentPosition(Long userId, Point userPoint, RemainingTime remainingTime, Double totalTravelDistance) {
-        TravelTrackingInfo info = infoStore.get(userId);
+        TravelTrackingInfo info = getInfoOrThrow(userId);
         info.currentPosition(userPoint, totalTravelDistance, remainingTime);
         log.info("산행 정보 업데이트. userId : {}", userId);
     }

     public void pause(Long userId, Point userPoint, RemainingTime remainingTime, Double totalTravelDistance) {
-        TravelTrackingInfo info = infoStore.get(userId);
+        TravelTrackingInfo info = getInfoOrThrow(userId);
         info.pause(userPoint, totalTravelDistance, remainingTime);
         log.info("산행 중지. userId : {}", userId);
     }

     public void reStart(Long userId, Point userPoint, RemainingTime remainingTime, Double totalTravelDistance) {
-        TravelTrackingInfo info = infoStore.get(userId);
+        TravelTrackingInfo info = getInfoOrThrow(userId);
         info.reStart(userPoint, totalTravelDistance, remainingTime);
         log.info("산행 재개. userId : {}", userId);
     }

     public TravelTrackingInfo end(LocalDateTime endAt, Long userId, Point userPoint, RemainingTime remainingTime, Double totalTravelDistance, Duration totalTravelTime) {
-        TravelTrackingInfo info = infoStore.get(userId);
+        TravelTrackingInfo info = getInfoOrThrow(userId);
         info.end(endAt, userPoint, totalTravelDistance, remainingTime, totalTravelTime);
         log.info("산행 종료. userId : {}", userId);
         return info;
     }
+
+    private TravelTrackingInfo getInfoOrThrow(Long userId) {
+        return Optional.ofNullable(infoStore.get(userId))
+                .orElseThrow(TravelTrackingInfoNotFoundException::new);
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void start(Long userId, RemainingTime remainingTime) {
TravelTrackingInfo info = infoStore.get(userId);
info.start(remainingTime);
log.info("산행 start. userId : {}", userId);
}
public void currentPosition(Long userId, Point userPoint, RemainingTime remainingTime, Double totalTravelDistance) {
TravelTrackingInfo info = infoStore.get(userId);
info.currentPosition(userPoint, totalTravelDistance, remainingTime);
log.info("산행 정보 업데이트. userId : {}", userId);
}
public void pause(Long userId, Point userPoint, RemainingTime remainingTime, Double totalTravelDistance) {
TravelTrackingInfo info = infoStore.get(userId);
info.pause(userPoint, totalTravelDistance, remainingTime);
log.info("산행 중지. userId : {}", userId);
}
public void reStart(Long userId, Point userPoint, RemainingTime remainingTime, Double totalTravelDistance) {
TravelTrackingInfo info = infoStore.get(userId);
info.reStart(userPoint, totalTravelDistance, remainingTime);
log.info("산행 재개. userId : {}", userId);
}
public TravelTrackingInfo end(LocalDateTime endAt, Long userId, Point userPoint, RemainingTime remainingTime, Double totalTravelDistance, Duration totalTravelTime) {
TravelTrackingInfo info = infoStore.get(userId);
info.end(endAt, userPoint, totalTravelDistance, remainingTime, totalTravelTime);
log.info("산행 종료. userId : {}", userId);
public void start(Long userId, RemainingTime remainingTime) {
TravelTrackingInfo info = getInfoOrThrow(userId);
info.start(remainingTime);
log.info("산행 start. userId : {}", userId);
}
public void currentPosition(Long userId, Point userPoint, RemainingTime remainingTime, Double totalTravelDistance) {
TravelTrackingInfo info = getInfoOrThrow(userId);
info.currentPosition(userPoint, totalTravelDistance, remainingTime);
log.info("산행 정보 업데이트. userId : {}", userId);
}
public void pause(Long userId, Point userPoint, RemainingTime remainingTime, Double totalTravelDistance) {
TravelTrackingInfo info = getInfoOrThrow(userId);
info.pause(userPoint, totalTravelDistance, remainingTime);
log.info("산행 중지. userId : {}", userId);
}
public void reStart(Long userId, Point userPoint, RemainingTime remainingTime, Double totalTravelDistance) {
TravelTrackingInfo info = getInfoOrThrow(userId);
info.reStart(userPoint, totalTravelDistance, remainingTime);
log.info("산행 재개. userId : {}", userId);
}
public TravelTrackingInfo end(LocalDateTime endAt, Long userId, Point userPoint, RemainingTime remainingTime, Double totalTravelDistance, Duration totalTravelTime) {
TravelTrackingInfo info = getInfoOrThrow(userId);
info.end(endAt, userPoint, totalTravelDistance, remainingTime, totalTravelTime);
log.info("산행 종료. userId : {}", userId);
return info;
}
private TravelTrackingInfo getInfoOrThrow(Long userId) {
return Optional.ofNullable(infoStore.get(userId))
.orElseThrow(TravelTrackingInfoNotFoundException::new);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/econo_4factorial/newproject/travel/service/TravelTrackingInfoStore.java`
around lines 36 - 63, All write methods (start, currentPosition, pause, reStart,
end) call infoStore.get(userId) without null checks and can NPE; update each
method to validate the result of infoStore.get(userId) and if null throw the
domain exception TravelTrackingInfoNotFoundException (same behavior as the
query/read methods) before invoking instance methods like info.start(...),
info.currentPosition(...), info.pause(...), info.reStart(...), info.end(...);
ensure the thrown exception includes the userId for context.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants