Conversation
trigger 브랜치를 dev에서 test로 변경
userId를 이용해서 조회하므로 findByUserId로 수정
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
Walkthrough지리 정보 시스템(GIS)으로 마이그레이션하면서 위도/경도를 JTS Point/LineString으로 변경합니다. 웹소켓 기반 실시간 여행 추적 및 기록 시스템을 추가하고, 거리 계산, 남은 시간 계산, 위치 편차 감지 등의 도메인 로직을 구현합니다. Changes
Sequence DiagramsequenceDiagram
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: 세션 정리
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes 이 변경사항은 광범위한 도메인 추가(여행 추적, GIS 마이그레이션), 복잡한 상태 관리(7개 상태, 6개 이벤트), WebSocket 실시간 통신, 다층 거리/시간 계산 로직, 다수의 새로운 서비스/맵퍼/DTO(50개 이상의 새 파일)를 포함합니다. 각 컴포넌트 간 상호작용이 밀집되어 있고, 기하학적 계산의 정확성, 상태 전이 유효성, 데이터 일관성 검증이 필수적입니다. Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
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. Comment |
There was a problem hiding this comment.
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와 불일치합니다.
RestartEventReq의time필드가Timestamp타입인데,StartEventReq와EndEventReq는Long타입을 사용합니다.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]에서
endAt를startedAt으로 변환하고 있어, 상세 응답의 종료 시각이 항상 잘못 내려갑니다. 종료 시각 필드로 매핑해 주세요.🤖 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.coordinates와TravelRecord.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도 동일 문제
PauseEventReq와RestartEventReq모두Timestamp time과double[] coordinate를 사용 중입니다.time은Instant또는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 | 🟠 MajorLong 객체 비교 시
==사용 - 잠재적 버그
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인증되지 않은 연결 종료 경로에서
nulluserId를 그대로 넘기고 있습니다.인증 전에 끊긴 세션이면
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_001→INTERNAL_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_001은CourseErrorType내부 코드 체계와 어긋나서 로그 집계/클라이언트 분기 시 혼선을 만들 수 있습니다.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 | 🟡 MinorGeoUtil.toLineString() 메서드에서 명시적 SRID 설정이 누락되었습니다.
src/main/java/com/econo_4factorial/newproject/travel/util/GeoUtil.java의toLineString()메서드(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수정 필요
mountainId가1로 하드코딩되어 있어 무등산 외 다른 산의 데이터 처리 시 문제가 발생합니다.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
📒 Files selected for processing (79)
.github/workflows/trigger-cicd.ymlbuild.gradlesrc/main/java/com/econo_4factorial/newproject/auth/controller/OAuthController.javasrc/main/java/com/econo_4factorial/newproject/auth/jwt/service/AuthTokenService.javasrc/main/java/com/econo_4factorial/newproject/base/domain/Base.javasrc/main/java/com/econo_4factorial/newproject/base/dto/BaseDTO.javasrc/main/java/com/econo_4factorial/newproject/base/service/weather/WeatherService.javasrc/main/java/com/econo_4factorial/newproject/common/annotation/resolver/UserIdResolver.javasrc/main/java/com/econo_4factorial/newproject/common/config/GeometryConfig.javasrc/main/java/com/econo_4factorial/newproject/common/config/WebSocketConfig.javasrc/main/java/com/econo_4factorial/newproject/common/exception/CommonErrorType.javasrc/main/java/com/econo_4factorial/newproject/common/exception/WebSocketIOException.javasrc/main/java/com/econo_4factorial/newproject/common/util/DateUtil.javasrc/main/java/com/econo_4factorial/newproject/common/util/TimeMapper.javasrc/main/java/com/econo_4factorial/newproject/common/util/webSocket/WebSocketAuthService.javasrc/main/java/com/econo_4factorial/newproject/common/util/webSocket/WebSocketFailRes.javasrc/main/java/com/econo_4factorial/newproject/common/util/webSocket/WebSocketResponser.javasrc/main/java/com/econo_4factorial/newproject/common/util/webSocket/WebSocketSuccessRes.javasrc/main/java/com/econo_4factorial/newproject/course/domain/Course.javasrc/main/java/com/econo_4factorial/newproject/course/dto/ClosestCoordinateInfo.javasrc/main/java/com/econo_4factorial/newproject/course/exception/CourseErrorType.javasrc/main/java/com/econo_4factorial/newproject/course/exception/InternalServerException/ClosestCoordinateNotFoundException.javasrc/main/java/com/econo_4factorial/newproject/course/repository/CourseRepository.javasrc/main/java/com/econo_4factorial/newproject/course/service/CourseLocationMatcher.javasrc/main/java/com/econo_4factorial/newproject/course/service/CourseService.javasrc/main/java/com/econo_4factorial/newproject/pathway/domain/Pathway.javasrc/main/java/com/econo_4factorial/newproject/pathway/dto/PathwayDTO.javasrc/main/java/com/econo_4factorial/newproject/pathway/dto/res/GetPathwaysOfCourseRes.javasrc/main/java/com/econo_4factorial/newproject/pathway/dto/res/PathwayCoordinatesArrayDTO.javasrc/main/java/com/econo_4factorial/newproject/pathway/mapper/PathwayMapper.javasrc/main/java/com/econo_4factorial/newproject/pathway/service/CoursePathwaySequenceService.javasrc/main/java/com/econo_4factorial/newproject/pathway/service/PathwayService.javasrc/main/java/com/econo_4factorial/newproject/travel/Status.javasrc/main/java/com/econo_4factorial/newproject/travel/TravelEvent.javasrc/main/java/com/econo_4factorial/newproject/travel/controller/TravelRecordController.javasrc/main/java/com/econo_4factorial/newproject/travel/domain/TravelRecord.javasrc/main/java/com/econo_4factorial/newproject/travel/domain/TravelTrackingInfo.javasrc/main/java/com/econo_4factorial/newproject/travel/domain/vo/RemainingTime.javasrc/main/java/com/econo_4factorial/newproject/travel/dto/Payload.javasrc/main/java/com/econo_4factorial/newproject/travel/dto/TravelAnalysisInput.javasrc/main/java/com/econo_4factorial/newproject/travel/dto/TravelAnalysisResult.javasrc/main/java/com/econo_4factorial/newproject/travel/dto/TravelRecordDTO.javasrc/main/java/com/econo_4factorial/newproject/travel/dto/TravelRecordDetailDTO.javasrc/main/java/com/econo_4factorial/newproject/travel/dto/req/CurrentPositionEventReq.javasrc/main/java/com/econo_4factorial/newproject/travel/dto/req/EndEventReq.javasrc/main/java/com/econo_4factorial/newproject/travel/dto/req/KeepAliveEventReq.javasrc/main/java/com/econo_4factorial/newproject/travel/dto/req/PauseEventReq.javasrc/main/java/com/econo_4factorial/newproject/travel/dto/req/RestartEventReq.javasrc/main/java/com/econo_4factorial/newproject/travel/dto/req/StartEventReq.javasrc/main/java/com/econo_4factorial/newproject/travel/dto/res/CurrentPositionEventRes.javasrc/main/java/com/econo_4factorial/newproject/travel/dto/res/EndEventRes.javasrc/main/java/com/econo_4factorial/newproject/travel/dto/res/GetTravelRecordByMonthRes.javasrc/main/java/com/econo_4factorial/newproject/travel/dto/res/GetTravelRecordDetailRes.javasrc/main/java/com/econo_4factorial/newproject/travel/dto/res/PauseEventRes.javasrc/main/java/com/econo_4factorial/newproject/travel/dto/res/RestartEventRes.javasrc/main/java/com/econo_4factorial/newproject/travel/dto/res/StartEventRes.javasrc/main/java/com/econo_4factorial/newproject/travel/dto/res/TravelEventResponse.javasrc/main/java/com/econo_4factorial/newproject/travel/dto/res/TravelEventResponseData.javasrc/main/java/com/econo_4factorial/newproject/travel/exception/NotAllowedEventForStatusException.javasrc/main/java/com/econo_4factorial/newproject/travel/exception/NotExistEventException.javasrc/main/java/com/econo_4factorial/newproject/travel/exception/TravelErrorType.javasrc/main/java/com/econo_4factorial/newproject/travel/exception/TravelRecordNotFoundException.javasrc/main/java/com/econo_4factorial/newproject/travel/exception/TravelTrackingInfoNotFoundException.javasrc/main/java/com/econo_4factorial/newproject/travel/exception/UnauthenticatedException.javasrc/main/java/com/econo_4factorial/newproject/travel/handler/WebSocketHandler.javasrc/main/java/com/econo_4factorial/newproject/travel/mapper/TravelMapper.javasrc/main/java/com/econo_4factorial/newproject/travel/repository/TravelRecordRepository.javasrc/main/java/com/econo_4factorial/newproject/travel/service/RemainingTimeCalculator.javasrc/main/java/com/econo_4factorial/newproject/travel/service/TravelDistanceCalculator.javasrc/main/java/com/econo_4factorial/newproject/travel/service/TravelDomainService.javasrc/main/java/com/econo_4factorial/newproject/travel/service/TravelRecordService.javasrc/main/java/com/econo_4factorial/newproject/travel/service/TravelService.javasrc/main/java/com/econo_4factorial/newproject/travel/service/TravelTrackingInfoStore.javasrc/main/java/com/econo_4factorial/newproject/travel/util/EventPolicy.javasrc/main/java/com/econo_4factorial/newproject/travel/util/GeoUtil.javasrc/main/java/com/econo_4factorial/newproject/travel/util/PayloadMapper.javasrc/main/java/com/econo_4factorial/newproject/travel/util/TravelResponseMapper.javasrc/main/resources/application-jpa.ymlsrc/main/resources/data.sql
src/main/java/com/econo_4factorial/newproject/auth/controller/OAuthController.java
Outdated
Show resolved
Hide resolved
| package com.econo_4factorial.newproject.course.repository; | ||
|
|
||
| import com.econo_4factorial.newproject.course.domain.Course; | ||
| import feign.Param; |
There was a problem hiding this comment.
🧩 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.javaRepository: 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 -5Repository: 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 -30Repository: 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 -30Repository: JNU-econovation/sangyeol-backend
Length of output: 58
🏁 Script executed:
# Examine build.gradle for Spring Data and Feign dependencies
cat build.gradleRepository: 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 5Repository: 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 javaRepository: 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 -20Repository: 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.
| 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` 애노테이션을 사용하도록 업데이트하세요.
| @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); | ||
| } |
There was a problem hiding this comment.
🧩 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 javaRepository: JNU-econovation/sangyeol-backend
Length of output: 161
🏁 Script executed:
# Search for findRecordById method implementation
rg "findRecordById" -t java -B 2 -A 10Repository: 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.javaRepository: 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 20Repository: 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 -70Repository: JNU-econovation/sangyeol-backend
Length of output: 3980
소유권 검증 없이 임의의 기록에 접근 가능한 보안 취약점
findRecordById 및 deleteRecordById 메서드가 userId를 사용하지 않아, 다른 사용자의 산행 기록을 조회하고 삭제할 수 있습니다. findRecordByMonth에서는 올바르게 userId로 필터링하는 것과 달리, 상세조회와 삭제 메서드는 recordId만으로 소유권 검증 없이 데이터에 접근합니다.
TravelRecordService의 findRecordById(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).
| @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); | ||
| } |
There was a problem hiding this comment.
소유권 검증 누락 - 보안 취약점
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.
| @Column(columnDefinition = "LINESTRING SRID 4326", nullable = false) | ||
| private LineString paths; |
There was a problem hiding this comment.
🧩 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 -5Repository: 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 -30Repository: 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 -100Repository: JNU-econovation/sangyeol-backend
Length of output: 2316
🏁 Script executed:
# Check build.gradle for complete dependency block
fd "build.gradle" | xargs cat 2>/dev/nullRepository: JNU-econovation/sangyeol-backend
Length of output: 2860
Hibernate Spatial 방언(Dialect) 설정을 공간 데이터 지원에 맞게 업데이트하세요.
hibernate-spatial:6.5.2.Final과 jts-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.
| public TravelRecordDetailDTO findRecordById(Long recordId) { | ||
| return travelRecordRepository.findById(recordId) | ||
| .map(TravelRecordDetailDTO::from) | ||
| .orElseThrow(TravelRecordNotFoundException::new); | ||
| } | ||
|
|
||
| @Transactional | ||
| public void deleteRecordById(Long recordId) { | ||
| travelRecordRepository.deleteById(recordId); |
There was a problem hiding this comment.
상세 조회/삭제에 소유권 검증을 강제할 수 없습니다.
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<>(); |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
저장 정보 미존재 시 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.
| 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.
#️⃣연관된 이슈
📝작업 내용
웹소켓을 통해 유저와 웹소켓을 연결하고 인증을 마무리하게 된다면, 길안내가 시작됩니다. 길안내 과정에서 중요한 서비스나 로직에 대해서 정리하였습니다
1. TravelService
전반적인 길안내를 담당하는 서비스입니다. 크게 동작 방식은 아래와 같습니다.
3번 과정에서 핸들러를 호출하여 길안내를 수행할 때, 핸들러가 현재 별도로 분리되어 있지 않고, TravelService에 합쳐져 있는 상태입니다. 이는 추후에 분리할 수 있으면 분리하면 좋겠네요
산행 이벤트별 동작 안내
1. start 이벤트
해당 이벤트는 길안내를 시작하는 이벤트입니다
TravelTrackingInfo를 만들게 됩니다.TravelTrackingInfo란, 말 그대로 해당 유저의 실시간 등산 정보를 담고 있는 것입니다.2.16km/h로 계산하게 됩니다TravelTrackingInfo정보 업데이트(남은 시간)2. currentPosition 이벤트
사용자가 등산을 하는 중으로, 실시간 위치를 보내오는 이벤트입니다.
TravelTrackingInfo정보 업데이트(남은 시간, 총 등산 거리, 등산 경로(path)에 현재 위치 추가, 상태를TRAVEL로 변경)3. pause 이벤트
사용자가 등산 중 일시 중지를 하는 이벤트입니다.
TravelTrackingInfo정보 업데이트(남은 시간, 총 등산 거리, 등산 경로(path)에 현재 위치 추가, 상태를PAUSED로 변경)4. reStart 이벤트
사용자가 일시 중지 상태에서 다시 등산을 재개하는 이벤트입니다
TravelTrackingInfo정보 업데이트(남은 시간, 총 등산 거리, 등산 경로(path)에 현재 위치 추가, 상태를RESTARTED로 변경)5. end 이벤트
사용자가 등산 길안내를 종료하는 이벤트입니다
TravelTrackingInfo정보 업데이트(남은 시간, 총 등산 거리, 등산 경로(path)에 현재 위치 추가, 상태를END로 변경, 총 등산 시간 저장, 종료 시간 저장)TravelRecord저장 : TravelTrackingInfo를 바탕으로 TravelRecord를 생성하여 등산 기록을 저장합니다.기타
등산 길안내 과정에서 핵심 로직들만 설명을 드렸습니다. 여러 보조 서비스들에 대한 설명은 하지 못했는데, 제가 이름은 잘 지어놓았고, 해당 보조 서비스들이 결국 위에 있는 핵심 로직들에서 책임 분리 측면에서 만들어낸 서비스들이라 이해하시는 데 무리가 없을 겁니다. (혹여 해당 서비스가 잘 이해가 안되신다면 다시 여쭤봐주세요)
base초기 데이터 삽입 시 위경도를 날것으로 삽입을 하였는데, 길안내 과정에서 해당 base 정보를 활용하니 바로 활용할 수 있도록geometry객체 형태로 삽입되도록 수정하였습니다.destination_base_id를 추가해줬습니다등산 기록을 의미하는 TravelRecord 도메인에는 간단 crud 밖에 별다른 설명은 하지 않아도 될 것 같아 생략합니다.
스크린샷 (선택)
💬리뷰 요구사항(선택)
Summary by CodeRabbit
새로운 기능
변경 사항