Skip to content

Conversation

@hyoinYang
Copy link
Contributor

@hyoinYang hyoinYang commented Aug 21, 2025

✔️ 연관 이슈

📝 작업 내용

대중교통 길찾기 api의 속도를 개선합니다.

참고: 공공데이터 API(버스 정보 출력하는 API)는 webclient로 리팩토링 완료했지만 일일트래픽 한도 때문에 더이상 테스트가 불가해 안정성을 위해 API 말고 더미데이터를 사용하게 되었습니다 🥹

스크린샷 (선택)

Summary by CodeRabbit

  • 신규 기능

    • 내부 경로 표현(InternalStep)과 노드 참조(NodeRef) 도입으로 API 응답의 단계별 메타데이터 확장
    • OpenData 전용 WebClient 연동 추가
  • 개선

    • 버스/정류장 처리 흐름을 실데이터 기반 WebClient로 전환해 정류장 식별 및 노선 연동 정확도 향상
    • 공간 조회에서 필요한 열만 조회하는 투영 사용으로 메모리·성능 최적화
    • 휠체어 위치 조회 경량화로 응답 속도 개선
    • 정류장/도착 데이터 모델에 ARS ID 등 추가 필드 반영
  • 버그 수정

    • 불완전한 역·시설 정보 처리 시 에러 대신 경고로 안정성 향상

@hyoinYang hyoinYang self-assigned this Aug 21, 2025
@hyoinYang hyoinYang added the 🔧 refactor 코드 리팩토링 label Aug 21, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 21, 2025

Walkthrough

OpenData용 WebClient 빈 추가, OpenDataProperties 필드/시그니처 변경. 내부 라우팅용 InternalStep 도입. 노드/엣지 경계박스 쿼리를 프로젝션으로 전환하고 BusInfoService·FacilityService 등에서 WebClient 및 경로/휠체어 조회 로직을 변경함.

Changes

Cohort / File(s) Summary
WebClient 구성 및 프로퍼티
src/main/java/.../common/config/WebClientConfig.java, src/main/java/.../external/opendata/OpenDataProperties.java
OpenData 전용 WebClient 빈(openDataWebClient()) 추가. OpenDataProperties에서 key/userAgent 제거 및 레코드 시그니처 변경.
OpenData 응답 DTO 확장
src/main/java/.../external/opendata/dto/OpenDataResponse.java, src/main/java/.../external/opendata/dto/StationSearchResponse.java
OpenData 응답 Item에 rtNm, stNm, arsId 추가. StationItem에 arsId 필드 추가(생성자 시그니처 변경).
버스 정보 서비스 → WebClient 전환
src/main/java/.../service/BusInfoService.java
HttpClient 기반 호출을 WebClient로 전환. arrivals 조회에 arsId 사용, 역 검색·최단거리 선택·노선명 필터링 로직 추가 및 관련 시그니처 변경.
내부 라우팅 모델 도입
src/main/java/.../direction/dto/InternalStep.java, src/main/java/.../service/TransportationService.java, src/main/java/.../direction/dto/NodeRef.java
InternalStep·NodeRef 레코드 추가. 라우팅 내부 모델을 InternalStep 리스트로 처리하고, 서비스 정보(enrich) 단계에서 API 응답용 Step으로 변환.
엔티티 팩토리 메서드 추가
src/main/java/.../direction/entity/transportation/Edge.java, src/main/java/.../direction/entity/transportation/Route.java
Route 포함 Edge 생성 팩토리 createEdgeWithRoute(...) 추가. Route 팩토리 createRoute(...) 추가(whlchairs 초기화).
리포지토리: 프로젝션 기반 쿼리
src/main/java/.../repository/EdgeRepository.java, src/main/java/.../repository/NodeRepository.java, src/main/java/.../repository/EdgeBoundingBoxProjection.java, src/main/java/.../repository/NodeBoundingBoxProjection.java
경계박스 조회를 엔티티 대신 projection 인터페이스(EdgeBoundingBoxProjection, NodeBoundingBoxProjection)로 반환하도록 쿼리·시그니처 변경 및 ORDER BY 추가.
리포지토리 보강 및 API 경량화
src/main/java/.../repository/RouteRepository.java, src/main/java/.../repository/WheelchairInfoRepository.java, src/main/java/.../repository/FacilityRepository.java
findRouteNameById(Long) 추가. 휠체어 위치 전용 조회 findWheelchairLocationsByRouteId(Long) 추가. FacilityRepository.findByNodeId의 명시적 @query 제거(파생쿼리 사용).
시설 서비스 로직 변경
src/main/java/.../service/FacilityService.java
휠체어 조회를 엔티티 목록→문자열 위치 목록으로 변경. Facility 조회 및 유효성 검사 흐름을 Optional 기반으로 수정하고 로깅 레벨·메시지 조정.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant TransportationService
  participant RoutingCore as Routing (InternalStep)
  participant Enricher as Enrichment
  participant BusInfo as BusInfoService
  participant Facility as FacilityService

  Client->>TransportationService: findRoutes(request)
  TransportationService->>RoutingCore: runDijkstra / merge / addTransferWalk (InternalStep[])
  RoutingCore-->>TransportationService: routes (List<List<InternalStep>>)
  TransportationService->>Enricher: enrichRoutesWithServiceInfo(internalSteps)
  loop each InternalStep
    Enricher->>BusInfo: getBusInfo(routeId/routeName, station context)
    Enricher->>Facility: getNodeInfo(nodeId/line)
    BusInfo-->>Enricher: bus details
    Facility-->>Enricher: facility details
  end
  Enricher-->>TransportationService: enriched Steps
  TransportationService-->>Client: TransportationResponseDto (routes)
Loading
sequenceDiagram
  autonumber
  participant Service as BusInfoService
  participant OpenData as OpenData WebClient
  participant RouteRepo as RouteRepository

  Service->>OpenData: stationByName(name) (GET)
  OpenData-->>Service: StationSearchResponse
  Service->>Service: findClosestStation(stations, x,y, routeId)
  alt has routeId
    Service->>RouteRepo: findRouteNameById(routeId)
    RouteRepo-->>Service: Optional<String> routeName
  end
  Service->>OpenData: arrivals(arsId) (GET)
  OpenData-->>Service: OpenDataResponse
  Service-->>Caller: BusInfo result (filtered by routeName if present)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
쿼리 최적화 (#167)
API 호출을 비동기 처리 (#167) WebClient 도입은 확인되나 호출이 전체 흐름에서 논블로킹 리액티브로 전파되는지(Controller→Service 비동기화 등)는 불명확.
API 호출 최소화 (#167) 호출 횟수 축소(캐시, 병렬 병합 등)가 코드상 명확히 구현되었는지 불명확.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
OpenDataProperties에서 key/userAgent 필드 제거 (src/main/java/com/wayble/server/direction/external/opendata/OpenDataProperties.java) 설정 필드 제거는 성능(쿼리·비동기·호출 최소화) 목표와 직접적인 연관이 없음—운영/구성 변경에 해당.
FacilityService 로깅 레벨·메시지 변경 (src/main/java/.../service/FacilityService.java) 로깅 톤 변경은 성능 최적화 요구사항과 관련 없는 운영성 변경임.

Possibly related PRs

Suggested labels

🛠️ fix

Suggested reviewers

  • KiSeungMin
  • zyovn
  • wonjun-lee-fcwj245

Poem

버니가 코드를 홉홉 뛰며 말하네 🐇
새 WebClient로 물길 또 바꿨지요
InternalStep으로 길을 속삭이고
경계는 좁아지고 조회는 가벼워졌네
당근 한 입, 배포 축하해요 🥕

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/hyoin

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/wayble/server/direction/service/TransportationService.java (1)

989-999: 환승 횟수 계산 로직 검토 필요

Lines 993-996에서 같은 mode와 같은 routeName인 경우에도 transferCount를 증가시키고 있습니다. 이는 의도된 동작인지 확인이 필요합니다.

-                    } else if (previousMode == step.mode() && 
-                        previousRouteName != null && step.routeName() != null &&
-                        previousRouteName.equals(step.routeName())) {
-                        transferCount++;
🧹 Nitpick comments (12)
src/main/java/com/wayble/server/direction/repository/NodeRepository.java (2)

12-16: Object[] 기반 프로젝션은 유지보수 비용이 큼 — 인터페이스/DTO 프로젝션으로 타입 안전성 확보 권장

현재 List<Object[]> 반환은 인덱스 순서 의존으로 실수 여지가 큽니다. Spring Data의 인터페이스 프로젝션(또는 DTO 생성자 프로젝션)으로 전환하면 가독성과 안정성이 개선됩니다. 아래처럼 alias를 부여하고 반환 타입을 프로젝션으로 바꾸는 것을 제안합니다.

적용 diff:

-    @Query("SELECT n.id, n.stationName, n.nodeType, n.latitude, n.longitude FROM Node n WHERE " +
+    @Query("SELECT n.id AS id, n.stationName AS stationName, n.nodeType AS nodeType, n.latitude AS latitude, n.longitude AS longitude FROM Node n WHERE " +
            "n.latitude BETWEEN :minLat AND :maxLat AND " +
            "n.longitude BETWEEN :minLon AND :maxLon " +
            "ORDER BY n.latitude, n.longitude")
-    List<Object[]> findNodesInBoundingBox(
+    List<NodeRepository.NodeBBoxProjection> findNodesInBoundingBox(

선택 사항(파일 외 추가): 인터페이스 프로젝션 선언

public interface NodeBBoxProjection {
    Long getId();
    String getStationName();
    com.wayble.server.direction.entity.type.DirectionType getNodeType();
    Double getLatitude();
    Double getLongitude();
}

15-15: ORDER BY가 필요한지 재검토 또는 인덱스 정렬 최적화

bbox 조회에서 정렬이 필수 요건이 아니라면 ORDER BY를 제거해 정렬 비용을 없애는 편이 빠릅니다. 정렬이 필요하다면 (latitude, longitude) 복합 인덱스를 추가해 정렬 비용을 최소화하세요.

선택 1: 정렬 제거

-           "ORDER BY n.latitude, n.longitude")
+           "")

선택 2: DB 인덱스 추가(권장)

  • JPA 엔티티에 인덱스
// Node.java
@Table(name = "node", indexes = {
    @Index(name = "idx_node_lat_lon", columnList = "latitude, longitude")
})
  • 또는 DDL
CREATE INDEX idx_node_lat_lon ON node (latitude, longitude);
src/main/java/com/wayble/server/direction/external/opendata/dto/StationSearchResponse.java (1)

16-21: arsId 추가는 적절 — Jackson 매핑을 명시화해 회귀 방지

공급자 필드명이 바뀌거나 대소문자 이슈가 생길 때를 대비해 @JsonProperty로 명시화하는 것을 권장합니다. 또한 arsId가 누락될 수 있으므로 서비스 레이어에서 null 안전 처리를 확인해 주세요.

적용 diff:

-    public record StationItem(
-            String arsId,
-            String stId,
-            String stNm,
-            String tmX,
-            String tmY
-    ) {}
+    public record StationItem(
+            @com.fasterxml.jackson.annotation.JsonProperty("arsId") String arsId,
+            @com.fasterxml.jackson.annotation.JsonProperty("stId") String stId,
+            @com.fasterxml.jackson.annotation.JsonProperty("stNm") String stNm,
+            @com.fasterxml.jackson.annotation.JsonProperty("tmX") String tmX,
+            @com.fasterxml.jackson.annotation.JsonProperty("tmY") String tmY
+    ) {}

추가 확인(선택): tmX/tmY가 숫자 연산에 사용된다면 Double/BigDecimal 변환 시 예외 처리 유무를 점검해 주세요.

src/main/java/com/wayble/server/direction/external/opendata/dto/OpenDataResponse.java (1)

37-41: rtNm/stNm/arsId 추가 취지 동의 — null/공백 값 처리 및 타입 일관성 점검

  • rtNm(노선명) 기반 필터링 시 null/공백/대소문자 차이(trim/equalsIgnoreCase) 처리 여부를 서비스에서 확인해 주세요.
  • 본 DTO의 busType1/2/term는 String, Arrival.java는 Integer로 보입니다. 동일 의미라면 한쪽으로 타입을 통일하거나, 서비스 레이어에서 안전한 파싱/매핑을 보장하세요. 파싱 실패 시 NFE 회피 로직을 두는 것이 안전합니다.
src/main/java/com/wayble/server/direction/entity/transportation/Route.java (1)

44-53: 정적 팩토리 추가는 명확하고 안전 — 컬렉션 초기화도 좋습니다

팩토리 메서드로 생성 경로를 통일하고 wheelchairs를 빈 컬렉션으로 초기화한 점 좋습니다. 이후 양방향 연관을 다룰 때 편의 메서드(addWheelchair 등)를 추가하면 일관성 유지에 도움이 됩니다.

src/main/java/com/wayble/server/common/config/WebClientConfig.java (1)

16-19: OpenDataProperties 바인딩 확인 및 WebClient 빈 주입 시 @qualifier 사용 권장

  • ServerApplication.java(20행)에서
    @EnableConfigurationProperties({TMapProperties.class, KricProperties.class, OpenDataProperties.class})
    어노테이션을 통해 OpenDataProperties가 스캔되어 바인딩 대상에 포함된 것을 확인했습니다. (검증 완료)

  • WebClientConfig.java에서 openDataWebClient(), tMapWebClient(), kricWebClient() 등 동일한 타입의 WebClient 빈이 3개 정의되어 있습니다.
    이들 빈을 주입하는 지점(생성자·필드·메서드 파라미터)에서
    @Qualifier("openDataWebClient"), @Qualifier("tMapWebClient"), @Qualifier("kricWebClient")
    와 같이 빈 이름을 명시하지 않으면 스프링이 어떤 빈을 선택해야 할지 판단하지 못해 AmbiguousBean 예외가 발생할 수 있으므로, 각 주입 지점에 @qualifier 사용을 권장드립니다.

    예시

    @Service
    public class SomeService {
        private final WebClient openDataWebClient;
    
        public SomeService(
            @Qualifier("openDataWebClient") WebClient openDataWebClient,
            @Qualifier("tMapWebClient")    WebClient tMapWebClient
        ) {
            this.openDataWebClient = openDataWebClient;
            // ...
        }
    }
src/main/java/com/wayble/server/direction/repository/WheelchairInfoRepository.java (1)

16-17: 중복 데이터 방지 및 페이로드 축소를 위해 DISTINCT 사용 권장

같은 노선(routeId)에 동일한 wheelchairLocation 값이 다수 행으로 존재할 수 있습니다. 호출 측에서 고유 값만 필요하다면 SELECT DISTINCT로 불필요한 중복을 제거하는 것이 성능/네트워크 측면에서 유리합니다.

-    @Query("SELECT w.wheelchairLocation FROM Wheelchair w WHERE w.route.routeId = :routeId")
+    @Query("SELECT DISTINCT w.wheelchairLocation FROM Wheelchair w WHERE w.route.routeId = :routeId")
     List<String> findWheelchairLocationsByRouteId(@Param("routeId") Long routeId);

부가 제안:

  • route_id, wheelchair_location에 복합 인덱스 추가를 고려하면 조회 성능이 더 개선됩니다.
src/main/java/com/wayble/server/direction/entity/transportation/Edge.java (1)

48-56: Edge 팩토리 메서드 오버로드 추가를 권장합니다

현재 IDENTITY 전략을 사용하는 Edge 엔티티의 팩토리(createEdgeWithRoute(Long id, …))에 외부에서 ID를 주입하면, 신규 영속 시 영속성 컨텍스트 오인 또는 PK 충돌 위험이 있습니다. 다행히 코드베이스 전반에서 이 메서드는 기존 데이터 재구성(조회용) 경로에서만 호출되고 있어(TransportationService.java 160–164), 당장 기능에는 문제가 없습니다.

• 호출 위치

  • src/main/java/com/wayble/server/direction/service/TransportationService.java:160–164

• 변경 제안

  1. 기존 시그니처는 유지하여 조회용 팩토리로 남깁니다.
  2. 신규 생성(영속)용으로 ID 없이 호출하는 오버로드를 추가하고, 신규 저장 코드에서는 이 무–ID 메서드만 사용하도록 가이드해 주세요.
     public static Edge createEdgeWithRoute(Long id, Node startNode, Node endNode, DirectionType edgeType, Route route) {
         return Edge.builder()
                 .id(id)
                 .edgeType(edgeType)
                 .startNode(startNode)
                 .endNode(endNode)
                 .route(route)
                 .build();
     }
+    // 신규 persist 용(권장): IDENTITY 전략에서 ID는 JPA에 위임
+    public static Edge createEdgeWithRoute(Node startNode, Node endNode, DirectionType edgeType, Route route) {
+        return Edge.builder()
+                .edgeType(edgeType)
+                .startNode(startNode)
+                .endNode(endNode)
+                .route(route)
+                .build();
+    }
src/main/java/com/wayble/server/direction/service/FacilityService.java (1)

19-19: 사용하지 않는 import 제거 필요

lombok.Builder가 import되었지만 코드에서 사용되지 않습니다.

-import lombok.Builder;
src/main/java/com/wayble/server/direction/service/BusInfoService.java (2)

115-133: block() 호출로 인한 성능 저하 가능성

WebClient를 사용하면서도 block() 호출로 동기 처리하고 있어 비동기 처리의 이점을 충분히 활용하지 못하고 있습니다. 완전한 비동기 처리를 위해서는 리액티브 스트림을 유지하거나 CompletableFuture를 사용하는 것을 고려해보세요.


269-295: findClosestStation의 성능 최적화 가능

routeId가 있을 때 최대 3개 정류소만 확인하도록 제한하여 API 호출을 줄였습니다. 하지만 각 정류소마다 fetchArrivals를 호출하면서 여전히 다수의 API 호출이 발생할 수 있습니다.

캐싱 메커니즘을 도입하거나 배치 API 호출을 고려해보세요.

src/main/java/com/wayble/server/direction/service/TransportationService.java (1)

710-712: 예외 처리 시 로그만 남기고 계속 진행

버스/지하철 정보 조회 실패 시 예외를 로그로만 남기고 처리를 계속합니다. 사용자에게 부분적인 정보라도 제공하려는 의도는 좋지만, 실패 이유를 사용자에게 알려주는 것도 고려해보세요.

Also applies to: 724-731

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3fe1ea9 and f079f3a.

📒 Files selected for processing (15)
  • src/main/java/com/wayble/server/common/config/WebClientConfig.java (2 hunks)
  • src/main/java/com/wayble/server/direction/dto/InternalStep.java (1 hunks)
  • src/main/java/com/wayble/server/direction/entity/transportation/Edge.java (1 hunks)
  • src/main/java/com/wayble/server/direction/entity/transportation/Route.java (2 hunks)
  • src/main/java/com/wayble/server/direction/external/opendata/OpenDataProperties.java (0 hunks)
  • src/main/java/com/wayble/server/direction/external/opendata/dto/OpenDataResponse.java (1 hunks)
  • src/main/java/com/wayble/server/direction/external/opendata/dto/StationSearchResponse.java (1 hunks)
  • src/main/java/com/wayble/server/direction/repository/EdgeRepository.java (1 hunks)
  • src/main/java/com/wayble/server/direction/repository/FacilityRepository.java (0 hunks)
  • src/main/java/com/wayble/server/direction/repository/NodeRepository.java (1 hunks)
  • src/main/java/com/wayble/server/direction/repository/RouteRepository.java (1 hunks)
  • src/main/java/com/wayble/server/direction/repository/WheelchairInfoRepository.java (1 hunks)
  • src/main/java/com/wayble/server/direction/service/BusInfoService.java (4 hunks)
  • src/main/java/com/wayble/server/direction/service/FacilityService.java (2 hunks)
  • src/main/java/com/wayble/server/direction/service/TransportationService.java (24 hunks)
💤 Files with no reviewable changes (2)
  • src/main/java/com/wayble/server/direction/external/opendata/OpenDataProperties.java
  • src/main/java/com/wayble/server/direction/repository/FacilityRepository.java
🧰 Additional context used
🧬 Code graph analysis (12)
src/main/java/com/wayble/server/direction/entity/transportation/Edge.java (2)
src/main/java/com/wayble/server/direction/entity/transportation/Node.java (2)
  • Entity (13-79)
  • createNode (63-65)
src/main/java/com/wayble/server/direction/entity/Edge.java (1)
  • Edge (5-11)
src/main/java/com/wayble/server/direction/entity/transportation/Route.java (2)
src/main/java/com/wayble/server/direction/entity/transportation/Wheelchair.java (1)
  • Entity (6-24)
src/main/java/com/wayble/server/direction/entity/transportation/Node.java (2)
  • Entity (13-79)
  • createNode (63-65)
src/main/java/com/wayble/server/direction/repository/RouteRepository.java (2)
src/main/java/com/wayble/server/direction/repository/FacilityRepository.java (2)
  • FacilityRepository (12-17)
  • Query (13-16)
src/main/java/com/wayble/server/direction/repository/ElevatorRepository.java (1)
  • ElevatorRepository (11-14)
src/main/java/com/wayble/server/common/config/WebClientConfig.java (1)
src/main/java/com/wayble/server/direction/external/opendata/OpenDataProperties.java (1)
  • ConfigurationProperties (5-16)
src/main/java/com/wayble/server/direction/repository/WheelchairInfoRepository.java (1)
src/main/java/com/wayble/server/direction/entity/transportation/Wheelchair.java (1)
  • Entity (6-24)
src/main/java/com/wayble/server/direction/dto/InternalStep.java (1)
src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java (6)
  • Step (21-30)
  • Schema (11-64)
  • Route (16-19)
  • MoveInfo (37-39)
  • SubwayInfo (47-51)
  • BusInfo (41-45)
src/main/java/com/wayble/server/direction/external/opendata/dto/OpenDataResponse.java (3)
src/main/java/com/wayble/server/direction/external/opendata/dto/Arrival.java (1)
  • Arrival (3-7)
src/main/java/com/wayble/server/direction/dto/response/BusInfo.java (2)
  • BusArrival (6-10)
  • BusInfo (5-11)
src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java (1)
  • Schema (11-64)
src/main/java/com/wayble/server/direction/service/TransportationService.java (1)
src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java (1)
  • Route (16-19)
src/main/java/com/wayble/server/direction/repository/NodeRepository.java (1)
src/main/java/com/wayble/server/direction/entity/transportation/Node.java (1)
  • Entity (13-79)
src/main/java/com/wayble/server/direction/repository/EdgeRepository.java (1)
src/main/java/com/wayble/server/direction/entity/Edge.java (1)
  • Edge (5-11)
src/main/java/com/wayble/server/direction/service/FacilityService.java (4)
src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java (1)
  • NodeInfo (59-63)
src/main/java/com/wayble/server/direction/entity/transportation/Wheelchair.java (1)
  • Entity (6-24)
src/main/java/com/wayble/server/direction/repository/FacilityRepository.java (1)
  • FacilityRepository (12-17)
src/main/java/com/wayble/server/direction/entity/transportation/Facility.java (1)
  • Entity (11-45)
src/main/java/com/wayble/server/direction/service/BusInfoService.java (3)
src/main/java/com/wayble/server/direction/service/FacilityService.java (1)
  • Service (33-162)
src/main/java/com/wayble/server/direction/dto/response/BusInfo.java (2)
  • BusInfo (5-11)
  • BusArrival (6-10)
src/main/java/com/wayble/server/direction/external/opendata/dto/Arrival.java (1)
  • Arrival (3-7)
🔇 Additional comments (13)
src/main/java/com/wayble/server/direction/repository/NodeRepository.java (1)

17-21: 경계값 정규화 확인됨

TransportationService.calculateBoundingBox 메서드에서 Math.min/Math.max를 이용해 start-end 순서와 관계없이 항상
minLat ≤ maxLat, minLon ≤ maxLon 형태로 바운딩 박스를 정규화하고 있습니다.

  • src/main/java/com/wayble/server/direction/service/TransportationService.java:214–219 (calculateBoundingBox)

따라서 호출부에서 별도 유효성 검증이나 정규화 로직이 없어도 안전합니다.

src/main/java/com/wayble/server/direction/repository/RouteRepository.java (1)

12-13: 스칼라 프로젝션으로 불필요한 엔티티 로딩 방지 — 좋습니다

routeName만 조회해 네트워크/영속성 컨텍스트 오버헤드를 줄이는 방향이 목표(PR 목표: 속도 개선)에 부합합니다. 반환 타입을 Optional으로 한 점도 적절합니다.

src/main/java/com/wayble/server/common/config/WebClientConfig.java (1)

47-49: URI 인코딩 모드 설정(LVALUES_ONLY)은 적절합니다

이미 인코딩된 쿼리 파라미터를 중복 인코딩하지 않도록 VALUES_ONLY 설정을 둔 부분은 적절합니다. 이 설정은 OpenData 계열 API에서 종종 필요한 방어적 조치입니다.

src/main/java/com/wayble/server/direction/service/FacilityService.java (2)

50-57: 휠체어 정보 조회 로직 개선됨

리포지토리에서 직접 String 리스트를 반환받도록 최적화되었습니다. 메모리 사용량과 처리 속도가 개선될 것으로 보입니다.


59-77: Facility 조회 최적화 및 오류 처리 개선

Node 조회 없이 직접 Facility를 조회하도록 변경되어 효율적입니다. 또한 로그 레벨이 적절하게 조정되었습니다 (error → warn/debug).

src/main/java/com/wayble/server/direction/service/BusInfoService.java (3)

17-18: WebClient 기반 비동기 처리로 전환

API 속도 개선을 위해 HttpClient에서 WebClient로 전환되었습니다. PR 목표인 "API 호출을 비동기 처리"와 일치합니다.


54-60: 정류소 선택 로직 최적화

정류소가 1개일 때 불필요한 거리 계산을 생략하여 API 호출을 최소화했습니다. PR 목표와 잘 부합합니다.


149-159: 루트명 기반 필터링 로직 개선

버스 노선을 routeName으로 필터링하는 로직이 추가되어 정확도가 향상되었습니다.

src/main/java/com/wayble/server/direction/service/TransportationService.java (5)

4-4: InternalStep DTO 도입으로 내부 처리 개선

내부 경로 처리를 위한 전용 DTO를 도입하여 관심사 분리가 잘 이루어졌습니다.


105-168: 쿼리 최적화 적용됨

프로젝션 기반 쿼리로 필요한 컬럼만 조회하도록 최적화되었습니다. PR 목표인 "쿼리 최적화"가 잘 구현되었습니다.


195-209: 메모리 관리 개선

finally 블록에서 명시적으로 메모리를 정리하고 필요시 GC를 호출하는 로직이 추가되었습니다. 대용량 데이터 처리 시 메모리 효율성이 향상될 것으로 보입니다.


692-749: 서비스 정보 조회를 위한 별도 메서드 분리

enrichRoutesWithServiceInfo 메서드로 버스/지하철 정보 조회 로직을 분리하여 코드의 가독성과 유지보수성이 향상되었습니다.


544-549: toMap 중복 키 처리 확인 및 special 노드 중복 등록 검토 필요

다음과 같이 ID –1L(시작 노드)와 –2L(종료 노드)가 두 번 추가되어,
buildGraph 메서드에서 Collectors.toMap(…, (existing, replacement) -> existing)을 사용할 경우 두 번째 노드 객체가 무시됩니다.

점검 지점:

  • src/main/java/com/wayble/server/direction/service/TransportationService.java
    • buildGraph 메서드 (약 544–549행)
    Map<Long, Node> nodeMap = nodes.stream()
        .collect(Collectors.toMap(
            Node::getId,
            node -> node,
            (existing, replacement) -> existing // 중복 시 기존 값 유지
        ));
  • src/main/java/com/wayble/server/direction/service/TransportationService.java
    • findMultipleTransportationRoutes 호출 전후 및 addOriginDestinationWalkConnections 메서드
    – 178–183행: startTmp/endTmp 추가 (nodes.add(startTmp); nodes.add(endTmp))
    – 505–509행: startNode/endNode 추가 (nodes.add(startNode); nodes.add(endNode))

위로 인해 동일 ID를 가진 두 번째 노드(startNode, endNode)가 모두 무시되는데,
이 동작이 의도된 것인지 확인이 필요합니다.
– 만약 walk 연결용 새 노드를 유지해야 한다면 merge function을
(existing, replacement) -> replacement으로 변경하거나
중복 노드 추가 전 필터링 로직을 도입하세요.

Comment on lines +12 to +13
@Query("SELECT r.routeName FROM Route r WHERE r.routeId = :routeId")
Optional<String> findRouteNameById(@Param("routeId") Long routeId);
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

메서드 명칭을 더 구체적으로: byId → byRouteId

JPA 기본 메서드(findById)와 의미가 혼동될 수 있어, byRouteId로의 리네이밍을 제안합니다. 기능 변화는 없습니다.

적용 diff:

-    @Query("SELECT r.routeName FROM Route r WHERE r.routeId = :routeId")
-    Optional<String> findRouteNameById(@Param("routeId") Long routeId);
+    @Query("SELECT r.routeName FROM Route r WHERE r.routeId = :routeId")
+    Optional<String> findRouteNameByRouteId(@Param("routeId") Long routeId);

호출부 업데이트 필요 여부를 아래로 확인해 주세요:


🏁 Script executed:

#!/bin/bash
# 메서드 사용처 확인
rg -n '\bfindRouteNameById\b' -C2

Length of output: 1679


메서드명 변경 시 호출부도 함께 갱신 필요

리팩토링한 메서드명(findRouteNameByRouteId)을 실제 호출부에서도 반영해야 합니다. 아래 위치에서 findRouteNameById를 모두 findRouteNameByRouteId로 변경해 주세요.

  • src/main/java/com/wayble/server/direction/service/BusInfoService.java:36
  • src/main/java/com/wayble/server/direction/service/BusInfoService.java:150

적용 예시:

--- a/src/main/java/com/wayble/server/direction/repository/RouteRepository.java
@@ -11,7 +11,7 @@
     @Query("SELECT r.routeName FROM Route r WHERE r.routeId = :routeId")
-    Optional<String> findRouteNameById(@Param("routeId") Long routeId);
+    Optional<String> findRouteNameByRouteId(@Param("routeId") Long routeId);
--- a/src/main/java/com/wayble/server/direction/service/BusInfoService.java
@@ -33,7 +33,7 @@
     if (busId != null) {
-        var routeName = routeRepository.findRouteNameById(busId);
+        var routeName = routeRepository.findRouteNameByRouteId(busId);
         isShuttleBus = routeName.isPresent() && routeName.get().contains("마포");
     }
@@ -148,7 +148,7 @@
     // busId로 route 정보 조회
-    var routeName = routeRepository.findRouteNameById(busId);
+    var routeName = routeRepository.findRouteNameByRouteId(busId);
     String projectRouteName = routeName.orElse(null);

이렇게 변경하면 JPA 기본 메서드와의 혼동을 피하고, 메서드 의도가 더 명확해집니다.

📝 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
@Query("SELECT r.routeName FROM Route r WHERE r.routeId = :routeId")
Optional<String> findRouteNameById(@Param("routeId") Long routeId);
@Query("SELECT r.routeName FROM Route r WHERE r.routeId = :routeId")
Optional<String> findRouteNameByRouteId(@Param("routeId") Long routeId);
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/direction/repository/RouteRepository.java
around lines 12-13 the repository method was renamed to findRouteNameByRouteId;
update all call sites to match: in
src/main/java/com/wayble/server/direction/service/BusInfoService.java at lines
36 and 150 replace calls to findRouteNameById(...) with
findRouteNameByRouteId(...), preserving the same parameters and handling of the
Optional return; ensure imports and compilation pass after the rename.

Copy link
Member

@seung-in-Yoo seung-in-Yoo left a comment

Choose a reason for hiding this comment

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

수고하셨습니다!! 그러면 저희 테스트만 더미 데이터를 사용하는건가요 아니면 대중교통 길찾기 자체에서도 더미 데이터로 사용하는건가요??

@hyoinYang
Copy link
Contributor Author

수고하셨습니다!! 그러면 저희 테스트만 더미 데이터를 사용하는건가요 아니면 대중교통 길찾기 자체에서도 더미 데이터로 사용하는건가요??

기능 자체(대중교통 길찾기의 버스 정보 출력)에서 더미 데이터를 사용합니다...!
일일트래픽 한도 때문에 더이상 테스트가 불가능한 상황이라 내일 데모데이 때 정보가 안 뜨는 것보단 더미데이터라도 사용하는 게 안정적이라고 생각했습니다...🥲

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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/wayble/server/direction/service/BusInfoService.java (1)

110-129: 더미 데이터의 비결정성은 테스트를 불안정하게 만듭니다

Math.random() 사용은 테스트 플래키의 원인이 됩니다. 입력 기반 시드로 결정적 값을 생성하세요.

적용 예:

-        isLowFloor.add(Math.random() < 0.7);
-        isLowFloor.add(Math.random() < 0.5);
-
-        Integer dispatchInterval = (int) (Math.random() * 15) + 1;
+        int seed = java.util.Objects.hash(stationName, busId);
+        java.util.Random rnd = new java.util.Random(seed);
+        isLowFloor.add(rnd.nextDouble() < 0.7);
+        isLowFloor.add(rnd.nextDouble() < 0.5);
+        Integer dispatchInterval = rnd.nextInt(15) + 1; // 1~15분
♻️ Duplicate comments (1)
src/main/java/com/wayble/server/direction/repository/EdgeRepository.java (1)

20-39: Object[] → 인터페이스 프로젝션 전환 및 안정적 정렬 추가: 매우 좋습니다

명시 alias와 EdgeBoundingBoxProjection 시그니처가 일치하고, ORDER BY e.id로 결과가 안정화되었습니다. 이전 리뷰 피드백을 잘 반영하셨습니다.

🧹 Nitpick comments (7)
src/main/java/com/wayble/server/direction/repository/NodeBoundingBoxProjection.java (1)

9-10: 좌표 타입을 primitive로 전환 고려(선택)

DB 컬럼이 nullable = false라면 프로젝션 리턴 타입을 double로 바꿔 박싱/언박싱을 피할 수 있습니다. 영향 범위가 작다면 추후 리팩토링 후보로 검토해 주세요.

src/main/java/com/wayble/server/direction/repository/EdgeRepository.java (1)

37-39: OR 조건의 범위 필터는 의도대로 보입니다만, 중복/성능 유의

시작/도착 노드 중 하나라도 사각형에 걸리면 포함하는 로직은 적절합니다. 다만 스타트/엔드 모두 박스 내부인 경우에도 행은 1개이므로 중복은 발생하지 않습니다. 성능상 필요하면 Node 좌표 인덱스 확인(앞선 코멘트) 및 페이징/슬라이싱 지원 메서드 추가를 고려해 주세요.

src/main/java/com/wayble/server/direction/service/BusInfoService.java (4)

131-171: 불필요한 변수 제거 + 타이핑된 역직렬화로 단순화 + 응답코드 검사 추가 권장

  • fullUri는 사용되지 않습니다. 제거하세요.
  • ObjectMapper로 문자열을 직접 파싱하기보다 bodyToMono(OpenDataResponse.class)가 간결하고 안전합니다.
  • 공공데이터 API는 HTTP 200이어도 msgHeader.resultCode로 오류를 반환합니다. 파싱 후 코드 확인을 권장합니다.

정리 예:

-            String fullUri = openDataProperties.baseUrl() + 
-                    openDataProperties.endpoints().arrivals() +
-                    "?serviceKey=" + serviceKey +
-                    "&arsId=" + stationId +
-                    "&resultType=json";
...
-            String rawResponse = openDataWebClient
+            OpenDataResponse originalResponse = openDataWebClient
                     .get()
                     .uri(uriBuilder -> uriBuilder
                             .path(openDataProperties.endpoints().arrivals())
                             .queryParam("serviceKey", serviceKey)
                             .queryParam("arsId", stationId)
                             .queryParam("resultType", "json")
                             .build())
                     .header("Accept", openDataProperties.accept())
                     .retrieve()
                     .onStatus(status -> status.isError(), response -> {
-                        return response.bodyToMono(String.class)
-                                .flatMap(body -> {
-                                    log.error("공공데이터 API 호출 오류: {}", body);
-                                    return Mono.error(new RuntimeException("API 호출 실패: " + response.statusCode()));
-                                });
+                        return response.bodyToMono(String.class).flatMap(body -> {
+                            log.error("공공데이터 API 호출 오류: {}", body);
+                            return Mono.error(new RuntimeException("API 호출 실패: " + response.statusCode()));
+                        });
                     })
-                    .bodyToMono(String.class)
+                    .bodyToMono(OpenDataResponse.class)
                     .block();
-                       
-            OpenDataResponse originalResponse = null;
-            if (rawResponse != null) {
-                try {
-                    ObjectMapper objectMapper = new ObjectMapper();
-                    originalResponse = objectMapper.readValue(rawResponse, OpenDataResponse.class);
-                } catch (Exception e) {
-                    log.error("JSON 파싱 실패: {}", e.getMessage());
-                }
-            }
+
+            if (originalResponse != null && originalResponse.msgHeader() != null
+                && !"0".equals(originalResponse.msgHeader().resultCode())) {
+                log.warn("공공데이터 API 응답 오류: code={}, msg={}",
+                         originalResponse.msgHeader().resultCode(),
+                         originalResponse.msgHeader().resultMessage());
+                return null;
+            }

205-247: fetchStationByName도 동일하게 단순화 가능하며, fullUri 제거 필요

  • fullUri 미사용 라인 제거.
  • bodyToMono(StationSearchResponse.class) 활용으로 파싱 단순화.
  • 응답 바디 내 오류 코드가 있다면 동일하게 검사하세요.

적용 예:

-            String fullUri = openDataProperties.baseUrl() + 
-                    openDataProperties.endpoints().stationByName() +
-                    "?serviceKey=" + serviceKey +
-                    "&stSrch=" + stationName +
-                    "&resultType=json";
...
-            String rawResponse = openDataWebClient
+            StationSearchResponse stationResponse = openDataWebClient
                     .get()
                     .uri(uriBuilder -> uriBuilder
                             .path(openDataProperties.endpoints().stationByName())
                             .queryParam("serviceKey", serviceKey)
                             .queryParam("stSrch", stationName)
                             .queryParam("resultType", "json")
                             .build())
                     .header("Accept", openDataProperties.accept())
                     .retrieve()
                     .onStatus(status -> status.isError(), response -> {
                         return response.bodyToMono(String.class)
                                 .flatMap(body -> {
                                     log.error("공공데이터 API 호출 오류: {}", body);
                                     return Mono.error(new RuntimeException("API 호출 실패: " + response.statusCode()));
                                 });
                     })
-                    .bodyToMono(String.class)
+                    .bodyToMono(StationSearchResponse.class)
                     .block();
-            
-            StationSearchResponse stationResponse = null;
-            if (rawResponse != null) {
-                try {
-                    ObjectMapper objectMapper = new ObjectMapper();
-                    stationResponse = objectMapper.readValue(rawResponse, StationSearchResponse.class);
-                } catch (Exception e) {
-                    log.error("JSON 파싱 실패: {}", e.getMessage());
-                }
-            }
-            
-            return stationResponse;
+            return stationResponse;

255-327: 거리 계산 중복 제거 및 상위 N개 정류소 검증의 병렬화(선택)

  • 현재 거리 계산이 정렬 이전/이후 두 번 수행됩니다. 한 번만 계산해 캐시하면 CPU 부담을 줄일 수 있습니다.
  • routeId가 있는 경우 상위 3개 정류소에 대해 fetchArrivals를 연속 호출합니다. 향후 실경로를 켜면 레이턴시가 증가합니다. WebClient를 유지하면서 Mono.zip 등으로 병렬 조회 후 첫 성공을 선택하는 전략을 고려해 주세요.

거리 캐싱 예:

-        List<StationSearchResponse.StationItem> sortedStations = new ArrayList<>();
+        class S { StationSearchResponse.StationItem i; double d; S(StationSearchResponse.StationItem i,double d){this.i=i;this.d=d;} }
+        List<S> tmp = new ArrayList<>();
...
-                double distance = Math.sqrt(Math.pow(stationX - x, 2) + Math.pow(stationY - y, 2));
-                // 거리 정보를 포함한 Wrapper 클래스 대신 정렬용 로직 사용
-                sortedStations.add(station);
+                double distance = Math.hypot(stationX - x, stationY - y);
+                tmp.add(new S(station, distance));
...
-        sortedStations.sort((s1, s2) -> {
-            try {
-                double d1 = Math.sqrt(Math.pow(Double.parseDouble(s1.tmX()) - x, 2) + Math.pow(Double.parseDouble(s1.tmY()) - y, 2));
-                double d2 = Math.sqrt(Math.pow(Double.parseDouble(s2.tmX()) - x, 2) + Math.pow(Double.parseDouble(s2.tmY()) - y, 2));
-                return Double.compare(d1, d2);
-            } catch (NumberFormatException e) {
-                return 0;
-            }
-        });
+        tmp.sort((a,b) -> Double.compare(a.d, b.d));
+        List<StationSearchResponse.StationItem> sortedStations = tmp.stream().map(s -> s.i).toList();

173-195: 노선명 매칭 로직 전반에 대한 정규화 비교 적용 권장

현재 projectRouteName.contains(item.rtNm()) || item.rtNm().contains(projectRouteName) 방식은 "10" vs "100" 등에서 오탐 발생 가능성이 높습니다.
아래 파일·라인에서 동일한 contains 기반 비교가 사용되므로, BusInfoService 뿐만 아니라 TransportationService 내 모든 비교 로직에 대해 통일된 정규화(숫자·영문·한글만 추출, 공백/접미사 제거 후 equalsIgnoreCase) 방식으로 개선하는 것을 권장드립니다.

• src/main/java/com/wayble/server/direction/service/BusInfoService.java:186
• src/main/java/com/wayble/server/direction/service/BusInfoService.java:310–311
• src/main/java/com/wayble/server/direction/service/TransportationService.java:381–382
• src/main/java/com/wayble/server/direction/service/TransportationService.java:989–990

간단 예시:

- return projectRouteName.contains(item.rtNm()) || item.rtNm().contains(projectRouteName);
+ String a = projectRouteName.replaceAll("[^0-9A-Za-z가-힣]", "");
+ String b = item.rtNm().replaceAll("[^0-9A-Za-z가-힣]", "");
+ return a.equalsIgnoreCase(b);

실제 노선명 포맷(예: "마포10", "10-1", "10번")을 대표하는 샘플 데이터로 검증 후 일관된 매칭 로직을 적용해 주세요.

src/main/java/com/wayble/server/direction/service/TransportationService.java (1)

691-749: 외부 API 호출을 enrichment 단계로 분리.

버스 정보와 지하철 시설 정보 조회를 별도 단계로 분리하여 비동기 처리 가능성을 열어두었습니다. PR 목표인 "API 호출을 비동기 처리"를 위한 좋은 기반이 마련되었습니다.

향후 CompletableFuture 등을 활용한 비동기 처리로 더욱 개선할 수 있을 것 같습니다.

비동기 처리 개선 예시:

private List<TransportationResponseDto.Step> enrichRoutesWithServiceInfo(List<InternalStep> steps) {
    List<CompletableFuture<TransportationResponseDto.Step>> futures = steps.stream()
        .map(step -> CompletableFuture.supplyAsync(() -> enrichSingleStep(step)))
        .collect(Collectors.toList());
        
    return futures.stream()
        .map(CompletableFuture::join)
        .collect(Collectors.toList());
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f079f3a and 341f573.

📒 Files selected for processing (6)
  • src/main/java/com/wayble/server/direction/repository/EdgeBoundingBoxProjection.java (1 hunks)
  • src/main/java/com/wayble/server/direction/repository/EdgeRepository.java (2 hunks)
  • src/main/java/com/wayble/server/direction/repository/NodeBoundingBoxProjection.java (1 hunks)
  • src/main/java/com/wayble/server/direction/repository/NodeRepository.java (2 hunks)
  • src/main/java/com/wayble/server/direction/service/BusInfoService.java (5 hunks)
  • src/main/java/com/wayble/server/direction/service/TransportationService.java (24 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
src/main/java/com/wayble/server/direction/repository/EdgeBoundingBoxProjection.java (1)
src/main/java/com/wayble/server/direction/entity/transportation/Edge.java (2)
  • Entity (8-47)
  • createEdge (38-46)
src/main/java/com/wayble/server/direction/repository/NodeBoundingBoxProjection.java (1)
src/main/java/com/wayble/server/direction/entity/transportation/Node.java (3)
  • Entity (13-79)
  • Node (55-61)
  • createNode (63-65)
src/main/java/com/wayble/server/direction/repository/NodeRepository.java (1)
src/main/java/com/wayble/server/direction/entity/transportation/Node.java (1)
  • Entity (13-79)
src/main/java/com/wayble/server/direction/service/BusInfoService.java (4)
src/main/java/com/wayble/server/direction/service/FacilityService.java (1)
  • Service (33-162)
src/main/java/com/wayble/server/direction/service/TransportationService.java (1)
  • Slf4j (26-1008)
src/main/java/com/wayble/server/direction/dto/response/BusInfo.java (2)
  • BusInfo (5-11)
  • BusArrival (6-10)
src/main/java/com/wayble/server/direction/external/opendata/OpenDataProperties.java (1)
  • Endpoints (15-15)
src/main/java/com/wayble/server/direction/service/TransportationService.java (1)
src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java (1)
  • Schema (11-64)
src/main/java/com/wayble/server/direction/repository/EdgeRepository.java (1)
src/main/java/com/wayble/server/direction/entity/transportation/Edge.java (1)
  • Entity (8-47)
🔇 Additional comments (18)
src/main/java/com/wayble/server/direction/repository/NodeBoundingBoxProjection.java (1)

5-11: 인터페이스 프로젝션 도입 적절 — JPQL alias와 시그니처가 정확히 일치합니다

getId/getStationName/getNodeType/getLatitude/getLongitude가 리포지토리 JPQL의 as id/stationName/nodeType/latitude/longitude와 매칭되어 안정적으로 매핑됩니다.

src/main/java/com/wayble/server/direction/repository/NodeRepository.java (3)

13-23: 프로젝션 기반 JPQL 전환 LGTM — 불필요한 엔티티 로딩 제거 + 정렬 추가 OK

필요한 컬럼만 선택하고 ORDER BY n.latitude, n.longitude로 결과 순서를 고정한 점 좋습니다. NodeBoundingBoxProjection과 alias가 정확히 일치합니다.


23-28: 프로젝션 타입 소비부 호환성 문제 없음 확인

findNodesInBoundingBox 호출부를 전수 점검한 결과, 유일한 소비부인 TransportationService에서 이미 List<NodeBoundingBoxProjection>로 올바르게 사용 중임을 확인했습니다. 추가 호출부도 발견되지 않아 컴파일 오류 우려가 없습니다.

  • src/main/java/com/wayble/server/direction/service/TransportationService.java:107 (호출부)
    List nodeRows = nodeRepository.findNodesInBoundingBox(...) — 정상

따라서 별도 수정 없이 머지 진행하셔도 됩니다.


20-22: 경계값 검증 로직 확인됨

  • TransportationService.javacalculateBoundingBox(Node start, Node end) 메서드(라인 212–217)에서 Math.min/Math.max를 이용해 minLat≤maxLat, minLon≤maxLon을 보장하고 있습니다. 별도 swap/검증 로직 추가는 불필요해 보입니다.

복합 인덱스 존재 여부 확인 요청

  • 현재 프로젝트 내 (src/main/resources 포함) 마이그레이션·DDL·XML 파일에서 node(latitude, longitude) 복합 인덱스 정의를 찾을 수 없습니다.
  • DB 스키마나 마이그레이션 도구(Flyway/Liquibase 등)에 해당 인덱스가 생성되어 있는지 확인 후, 없으면 성능 최적화를 위해 추가를 검토해 주세요.
src/main/java/com/wayble/server/direction/repository/EdgeRepository.java (1)

31-32: Route 엔티티 PK는 routeId로 정의되어 있어 문제 없습니다.

src/main/java/com/wayble/server/direction/entity/transportation/Route.java에서 @Id private Long routeId;로 선언되어 있어, JPQL의 r.routeId 사용은 올바릅니다. 추가 변경 없이 머지 가능합니다.

src/main/java/com/wayble/server/direction/repository/EdgeBoundingBoxProjection.java (1)

5-18: 확인 완료: getRouteId/getRouteName null 체크 적용됨

검색 결과, EdgeBoundingBoxProjection의 getRouteId()getRouteName() 호출 전 null 검사가 모든 소비부 코드 경로에 적절히 적용되어 있음을 확인했습니다.

  • BusInfoService
    • route.isPresent() 여부로 getRouteName() 호출 여부 결정 (라인 117, 299)
    • projectRouteName != null 체크 후 사용 (라인 301)
  • TransportationService
    • row.getRouteId() != null 검사 후 row.getRouteName() 전달 (라인 148–154)
    • e.getRoute() != null && e.getRoute().getRouteName() != null 검사 후 반환 (라인 948–951)

위와 같이 모든 경로에서 null 가능성을 안전하게 대비하고 있으므로, 추가 조치 없이 코드 변경을 승인합니다.

src/main/java/com/wayble/server/direction/service/TransportationService.java (12)

4-4: 새로운 InternalStep DTO 추가 확인.

내부 라우팅 표현을 위한 새로운 DTO가 추가되었습니다. API 응답과 내부 로직의 분리가 잘 이루어져 성능 개선에 기여할 것으로 보입니다.


12-14: 프로젝션 인터페이스 활용 승인.

필요한 컬럼만 조회하는 프로젝션 패턴을 적용하여 메모리 사용량과 네트워크 트래픽을 줄였습니다. 성능 최적화 목표에 부합합니다.


65-65: 내부 표현 타입 변경 확인.

List<List<InternalStep>>로 내부 라우팅 표현이 변경되었습니다. API 응답 타입은 유지하면서 내부 처리 로직이 개선되었습니다.


82-91: API 호출 최적화를 위한 enrichment 패턴 도입.

기존에는 모든 step에 대해 외부 API를 호출했지만, 이제 페이징된 경로에 대해서만 enrichment를 수행하여 불필요한 API 호출을 줄였습니다. PR 목표인 API 호출 최소화가 잘 구현되었습니다.


100-210: 메모리 관리와 프로젝션 활용이 우수합니다.

프로젝션을 활용한 데이터 로딩과 명시적 메모리 정리가 잘 구현되었습니다. 특히:

  1. 프로젝션으로 필요한 컬럼만 조회
  2. finally 블록에서 확실한 메모리 정리
  3. GC 조건부 호출로 메모리 압박 상황 대응

성능 개선과 안정성 확보에 효과적입니다.


544-549: 중복 키 처리 개선.

nodeMap 생성 시 중복 키가 발생할 경우 기존 값을 유지하는 로직이 추가되었습니다. 데이터 일관성 확보에 도움이 됩니다.


557-559: PriorityQueue 크기 최적화.

PriorityQueue의 초기 크기를 동적으로 조정하여 메모리 사용량을 최적화했습니다. 대용량 데이터 처리 시 유용합니다.


674-675: 요청 ID 추가로 추적성 개선.

requestId를 추가하여 경로 병합 과정의 추적성을 개선했습니다. 디버깅과 로그 분석에 도움이 될 것입니다.


752-851: InternalStep 기반 경로 병합 로직 개선.

경로 병합 로직이 새로운 InternalStep을 기반으로 개선되었습니다. routeId, startNode, endNode 등 추가 메타데이터를 포함하여 더 정확한 경로 분석이 가능해졌습니다.


701-708: 외부 API 호출 시 null 체크 강화 확인.

버스 정보 조회 시 routeId와 startNode에 대한 null 체크가 추가되어 안정성이 개선되었습니다.


709-712: 예외 처리 개선 확인.

외부 API 호출 실패 시 적절한 로깅과 함께 graceful degradation이 구현되었습니다. 서비스 안정성 향상에 기여합니다.


980-1005: 환승 계산 로직이 InternalStep에 맞게 수정됨.

새로운 데이터 구조에 맞게 환승 횟수 계산 로직이 적절히 수정되었습니다. 기존 로직의 의도를 유지하면서 새로운 구조에 맞게 잘 적응시켰습니다.

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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
src/main/java/com/wayble/server/direction/service/TransportationService.java (7)

472-514: 임시 노드(-1/-2) 중복 생성으로 인한 비정상 중복/오버라이드 가능성

findMultipleTransportationRoutes에서 이미 startTmp/endTmp를 nodes에 추가한 뒤, buildGraph.addOriginDestinationWalkConnections에서 동일 ID(-1/-2)로 새 Node를 또 생성/추가합니다. 그래프 맵 put 재할당/노드 리스트 중복으로 오동작/성능 저하가 발생할 수 있습니다.

기존 임시 노드를 재사용하도록 수정 제안:

-        Node startNode = Node.createNode(-1L, startTmp.getStationName(), DirectionType.WALK, 
-                startTmp.getLatitude(), startTmp.getLongitude());
-        Node endNode = Node.createNode(-2L, endTmp.getStationName(), DirectionType.WALK, 
-                endTmp.getLatitude(), endTmp.getLongitude());
+        Node startNode = startTmp;
+        Node endNode = endTmp;
@@
-        nodes.add(startNode);
-        nodes.add(endNode);
+        // 이미 상위에서 nodes에 추가됨: 중복 추가 지양

부가적으로, 그래프 초기화 시(-1/-2 키에 대해) 기존 리스트가 있다면 덮어쓰지 않도록 computeIfAbsent 사용을 검토해 주세요.


591-616: 동적 WALK 엣지 이중 완화(relaxation)로 인한 중복 작업

현재 블록에서 WALK 엣지를 추가하면서 곧바로 alt를 계산/완화하고, 이후 4단계 “기존 엣지 처리”에서 동일 엣지를 다시 순회/완화합니다. 결과의 정합성은 유지되더라도 우선순위 큐 삽입/비교가 2회 발생해 불필요한 오버헤드입니다. 또한 weightMap에 기록하지 않아 분기별 일관성도 떨어집니다.

  • 즉시 완화 코드를 제거하고,
  • weightMap에 동적 WALK 엣지 가중치를 기록한 뒤,
  • “기존 엣지 처리” 루프에서 단일 경로로 완화되게 하세요.
-                    if (walkDistance <= NEARBY_STATION_WALK_DISTANCE) {
-                        Edge walkEdge = Edge.createEdge(-3L, curr, nearbyNode, DirectionType.WALK);
-                        currentEdges.add(walkEdge);
-                        
-                        int weight = (int)walkDistance + STEP_PENALTY;
-                        int alt = distance.get(curr.getId()) + weight;
-                        if (alt < distance.get(nearbyNode.getId())) {
-                            distance.put(nearbyNode.getId(), alt);
-                            prevNode.put(nearbyNode.getId(), curr);
-                            prevEdge.put(nearbyNode.getId(), walkEdge);
-                            pq.add(nearbyNode);
-                        }
-                    }
+                    if (walkDistance <= NEARBY_STATION_WALK_DISTANCE) {
+                        Edge walkEdge = Edge.createEdge(-3L, curr, nearbyNode, DirectionType.WALK);
+                        currentEdges.add(walkEdge);
+                        int weight = (int) walkDistance + STEP_PENALTY;
+                        // 동적 엣지 가중치를 weightMap에 등록: 이후 공통 루프에서 일관 처리
+                        weightMap.put(Pair.of(curr.getId(), nearbyNode.getId()), weight);
+                    }

329-353: 경로에서 사용된 엣지 추출 로직의 O(VE) 이름 기반 매칭 — ID 기반으로 치환 필요

stationName 문자열 비교로 그래프 전체를 스캔하면 성능/정확도 모두 취약합니다(동명이역, 문자열 정규화 문제). InternalStep에 startNode/endNode가 있으므로 ID 기반 매칭이 가능합니다.

다음과 같이 치환을 제안합니다:

-    private Set<Pair<Long, Long>> extractActualEdgesFromRoute(List<InternalStep> route, Map<Long, List<Edge>> graph) {
-        Set<Pair<Long, Long>> usedEdges = new HashSet<>();
-        
-        for (InternalStep step : route) {
-            String fromName = step.from();
-            String toName = step.to();
-            
-            for (Map.Entry<Long, List<Edge>> entry : graph.entrySet()) {
-                Long nodeId = entry.getKey();
-                List<Edge> edges = entry.getValue();
-                
-                for (Edge edge : edges) {
-                    Node fromNode = edge.getStartNode();
-                    Node toNode = edge.getEndNode();
-                    
-                    if ((fromNode.getStationName().equals(fromName) && toNode.getStationName().equals(toName)) ||
-                        (fromNode.getStationName().equals(toName) && toNode.getStationName().equals(fromName))) {
-                        usedEdges.add(Pair.of(fromNode.getId(), toNode.getId()));
-                        usedEdges.add(Pair.of(toNode.getId(), fromNode.getId()));
-                    }
-                }
-            }
-        }
-        
-        return usedEdges;
-    }
+    private Set<Pair<Long, Long>> extractActualEdgesFromRoute(List<InternalStep> route, Map<Long, List<Edge>> graph) {
+        Set<Pair<Long, Long>> usedEdges = new HashSet<>();
+        for (InternalStep step : route) {
+            NodeRef s = step.startNode();
+            NodeRef e = step.endNode();
+            if (s != null && e != null && s.id() != null && e.id() != null) {
+                usedEdges.add(Pair.of(s.id(), e.id()));
+                usedEdges.add(Pair.of(e.id(), s.id()));
+            }
+        }
+        return usedEdges;
+    }

의존하는 다른 로직은 없습니다. 이름 기반 매칭은 제거 가능합니다.


418-426: 도보거리 계산이 상수 500m로 고정 — InternalStep.moveNumber 활용

WALK step의 moveNumber에 실제 미터 단위가 세팅되므로 이를 합산하세요.

-    private int calculateWalkDistance(List<InternalStep> route) {
-        return route.stream()
-                .filter(step -> step.mode() == DirectionType.WALK)
-                .mapToInt(step -> {
-                    // 간단한 도보 거리 추정 (실제로는 정확한 거리 계산 필요)
-                    return 500; // 기본값
-                })
-                .sum();
-    }
+    private int calculateWalkDistance(List<InternalStep> route) {
+        return route.stream()
+                .filter(step -> step.mode() == DirectionType.WALK)
+                .mapToInt(step -> Math.max(0, step.moveNumber() == null ? 0 : step.moveNumber()))
+                .sum();
+    }

756-814: mergeConsecutiveRoutes: WALK 스텝 생성 시 NodeRef 변환 NPE 및 보조 정보

walkStartNode/walkEndNode가 null일 경우 현재 코드에서 toNodeRef 호출이 NPE를 유발합니다(위 toNodeRef 수정으로 방어 가능). 또한 moveInfo=null 처리와 routeName/moveNumber 설정은 합리적입니다.

안전성 강화를 위해 아래처럼 약간 정리합니다(위 toNodeRef 수정 전제):

-                mergedSteps.add(new InternalStep(
-                    DirectionType.WALK, null, null, walkDistance, null, null, fromName, toName, null, toNodeRef(walkStartNode), toNodeRef(walkEndNode)
-                ));
+                mergedSteps.add(new InternalStep(
+                    DirectionType.WALK,
+                    /* moveInfo */ null,
+                    /* routeName */ null,
+                    /* moveNumber(m) */ walkDistance,
+                    /* busInfo */ null,
+                    /* subwayInfo */ null,
+                    fromName,
+                    toName,
+                    /* routeId */ null,
+                    toNodeRef(walkStartNode),
+                    toNodeRef(walkEndNode)
+                ));

추가로, 메서드 시그니처의 requestId 파라미터는 사용되지 않습니다. 제거를 권장합니다.


858-898: 환승 WALK step 생성 시 위치 계산을 Edge 탐색 대신 NodeRef로

현재는 calculateTransferWalkDistance(fromName, toName, pathEdges)로 그래프 엣지를 다시 뒤져서 좌표를 찾습니다. 이미 currentStep.endNode()nextStep.startNode()에 좌표가 포함되어 있어 직접 계산이 더 정확/효율적입니다. 또한 생성하는 WALK step에 NodeRef를 세팅해 주세요.

-                // 다음 step도 walk가 아닌 경우 (bus -> subway, subway -> bus 등)
+                // 다음 step도 walk가 아닌 경우 (bus -> subway, subway -> bus 등)
                 if (nextStep.mode() != DirectionType.WALK) {
                     // 환승 walk step 추가
                     String transferFrom = currentStep.to();
                     String transferTo = nextStep.from();
                     
-                    // 이전 step의 도착지와 다음 step의 출발지 사이의 직선거리 계산
-                    int walkDistance = calculateTransferWalkDistance(transferFrom, transferTo, pathEdges);
+                    // NodeRef 기반 직선거리 계산
+                    int walkDistance = calculateTransferWalkDistance(currentStep, nextStep);
                     
                     InternalStep walkStep = new InternalStep(
                         DirectionType.WALK,
                         null,
                         null,
                         walkDistance,
                         null,
                         null,
                         transferFrom,
-                        transferTo,
-                        null,
-                        null,
-                        null
+                        transferTo,
+                        /* routeId */ null,
+                        /* startNode */ currentStep.endNode(),
+                        /* endNode */ nextStep.startNode()
                     );
                     
                     result.add(walkStep);
                 }

그리고 아래 보조 메서드를 추가해 주세요(파일 내 적절한 위치):

// 새로 추가
private int calculateTransferWalkDistance(InternalStep currentStep, InternalStep nextStep) {
    NodeRef from = currentStep != null ? currentStep.endNode() : null;
    NodeRef to = nextStep != null ? nextStep.startNode() : null;
    if (from != null && to != null && from.latitude() != null && from.longitude() != null
            && to.latitude() != null && to.longitude() != null) {
        double km = haversine(from.latitude(), from.longitude(), to.latitude(), to.longitude());
        return (int) (km * 1000);
    }
    // fallback: 이름/edge 기반 탐색이 필요하면 기존 메서드 재사용
    return calculateTransferWalkDistance(
            currentStep != null ? currentStep.to() : null,
            nextStep != null ? nextStep.from() : null,
            /* pathEdges */ Collections.emptyList()
    );
}

985-1011: 환승 횟수 계산 로직이 과다 계수됨 — 모든 연속 스텝에서 증가

현재 분기 구조는 같은 모드/같은 노선, 같은 모드/다른 노선, 다른 모드 모두에서 증가시키고 있어 첫 운송 스텝 이후 매 스텝마다 카운트가 증가합니다. 실제 환승 정의(모드 변경 또는 노선 변경)에 맞게 수정해야 합니다.

-    private int calculateTransferCount(List<InternalStep> steps) {
-        int transferCount = 0;
-        DirectionType previousMode = null;
-        String previousRouteName = null;
-        
-        for (InternalStep step : steps) {
-            if (step.mode() != DirectionType.WALK && step.mode() != DirectionType.FROM_WAYPOINT && step.mode() != DirectionType.TO_WAYPOINT) {
-                if (previousMode != null) {
-                    if (previousMode == step.mode() && 
-                        previousRouteName != null && step.routeName() != null &&
-                        !previousRouteName.equals(step.routeName())) {
-                        transferCount++;
-                    } else if (previousMode == step.mode() && 
-                        previousRouteName != null && step.routeName() != null &&
-                        previousRouteName.equals(step.routeName())) {
-                        transferCount++;
-                    } else if (previousMode != step.mode()) {
-                        transferCount++;
-                    }
-                }
-                previousMode = step.mode();
-                previousRouteName = step.routeName();
-            }
-        }
-        return transferCount;
-    }
+    private int calculateTransferCount(List<InternalStep> steps) {
+        int transferCount = 0;
+        DirectionType prevMode = null;
+        String prevRouteName = null;
+        for (InternalStep step : steps) {
+            DirectionType mode = step.mode();
+            if (mode == DirectionType.WALK || mode == DirectionType.FROM_WAYPOINT || mode == DirectionType.TO_WAYPOINT) {
+                continue;
+            }
+            if (prevMode != null) {
+                // 모드가 바뀌면 환승
+                if (prevMode != mode) {
+                    transferCount++;
+                } else {
+                    // 같은 모드면 노선명이 달라질 때만 환승
+                    if (!Objects.equals(prevRouteName, step.routeName())) {
+                        transferCount++;
+                    }
+                }
+            }
+            prevMode = mode;
+            prevRouteName = step.routeName();
+        }
+        return transferCount;
+    }

테스트 케이스 추가를 제안합니다: [BUS A]→[BUS A] (0), [BUS A]→[BUS B] (1), [BUS A]→[SUBWAY X] (1), WALK 포함 케이스(0 유지).

원하시면 제가 파라미터라이즈드 테스트 템플릿을 드리겠습니다.

🧹 Nitpick comments (6)
src/main/java/com/wayble/server/direction/dto/NodeRef.java (1)

6-7: Primitive 사용 검토: Double → double 전환 고려

위도/경도는 내부 파이프라인에서 null일 가능성이 낮아 보입니다. 오토박싱/널 체크 비용을 줄이려면 double로 전환을 고려해 주세요. 외부 연동에서 null이 실제로 발생한다면 현재 형태를 유지하는 것이 맞습니다.

src/main/java/com/wayble/server/direction/service/TransportationService.java (5)

185-214: finally에서 명시적 GC 호출 및 리스트 null 처리 — 과도한 메모리 관리

명시적 System.gc() 호출과 수동 null 할당은 HotSpot에서 성능에 악영향을 줄 수 있습니다. 컬렉션은 스코프 종료로 자연 해제되며, OOM 처리와 무관합니다. 메모리 압박 상황에서도 조기 GC 강제는 일반적으로 권장되지 않습니다.

다음 정도로 단순화 추천:

-            if (nodes != null) {
-                nodes.clear();
-                nodes = null;
-            }
-            if (edges != null) {
-                edges.clear();
-                edges = null;
-            }
-
-            // 명시적 GC 호출
-            if (Runtime.getRuntime().freeMemory() < Runtime.getRuntime().totalMemory() * 0.1) {
-                System.gc();
-            }
+            // GC/명시적 null 지양: 로컬 변수는 메서드 종료 시 수거됨

373-392: 경로 동일성 비교에서 routeId 고려 누락

routeName이 동일하지만 서로 다른 노선(routeId)이면 동일 경로로 잘못 판단할 수 있습니다. routeId 우선 비교를 권장합니다.

-        for (int i = 0; i < route1.size(); i++) {
-            InternalStep step1 = route1.get(i);
-            InternalStep step2 = route2.get(i);
-            
-            if (step1.mode() != step2.mode() || 
-                !Objects.equals(step1.from(), step2.from()) || 
-                !Objects.equals(step1.to(), step2.to()) ||
-                !Objects.equals(step1.routeName(), step2.routeName())) {
-                return false;
-            }
-        }
+        for (int i = 0; i < route1.size(); i++) {
+            InternalStep s1 = route1.get(i);
+            InternalStep s2 = route2.get(i);
+            if (s1.mode() != s2.mode()) return false;
+            if (!Objects.equals(s1.from(), s2.from())) return false;
+            if (!Objects.equals(s1.to(), s2.to())) return false;
+            // routeId를 우선 비교
+            if (!Objects.equals(s1.routeId(), s2.routeId())) return false;
+            // 이름은 보조 비교
+            if (!Objects.equals(s1.routeName(), s2.routeName())) return false;
+        }

900-933: 기존 이름 기반 환승 거리 계산은 fallback 용도로만 유지

위 개선을 적용하면 본 메서드는 예외 케이스용 보조 수단으로만 남습니다. 가능하면 pathEdges를 순회하는 O(E) 탐색을 제거하거나, Node 이름→좌표 캐시 맵으로 치환해 주세요.


218-224: 경계박스 경도 보정에 평균 위도 사용 고려

현재 경도 스케일 보정에 start.getLatitude()만 사용합니다. 출발/도착 간 위도 차가 크면 왜곡이 생길 수 있어 평균 위도를 쓰는 편이 안정적입니다.

-        double minLon = Math.min(start.getLongitude(), end.getLongitude()) - SPATIAL_BUFFER_KM / (111.0 * Math.cos(Math.toRadians(start.getLatitude())));
-        double maxLon = Math.max(start.getLongitude(), end.getLongitude()) + SPATIAL_BUFFER_KM / (111.0 * Math.cos(Math.toRadians(start.getLatitude())));
+        double midLat = (start.getLatitude() + end.getLatitude()) / 2.0;
+        double minLon = Math.min(start.getLongitude(), end.getLongitude()) - SPATIAL_BUFFER_KM / (111.0 * Math.cos(Math.toRadians(midLat)));
+        double maxLon = Math.max(start.getLongitude(), end.getLongitude()) + SPATIAL_BUFFER_KM / (111.0 * Math.cos(Math.toRadians(midLat)));

696-754: enrichRoutesWithServiceInfo: 서비스 장애 내성 확보, 로깅 수준 점검

예외 캡처 후 기본/빈 값 제공으로 회복 가능성을 확보한 점 좋습니다. 운영 시 INFO 로그는 과다할 수 있어 DEBUG로 내리는 것도 고려해 주세요(특히 루프 내 호출).

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 341f573 and 98962d3.

📒 Files selected for processing (3)
  • src/main/java/com/wayble/server/direction/dto/InternalStep.java (1 hunks)
  • src/main/java/com/wayble/server/direction/dto/NodeRef.java (1 hunks)
  • src/main/java/com/wayble/server/direction/service/TransportationService.java (25 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/wayble/server/direction/dto/InternalStep.java
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/com/wayble/server/direction/dto/NodeRef.java (1)
src/main/java/com/wayble/server/direction/entity/transportation/Node.java (2)
  • Entity (13-79)
  • Node (55-61)
src/main/java/com/wayble/server/direction/service/TransportationService.java (1)
src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java (2)
  • Schema (11-64)
  • Route (16-19)
🔇 Additional comments (5)
src/main/java/com/wayble/server/direction/dto/NodeRef.java (1)

3-8: 간결한 내부 DTO 도입, 불변성/의도 명확 — LGTM

간단한 record로 내부 전달용 노드 레퍼런스를 분리한 점 좋습니다. 불변성 유지와 직관적인 필드 구성으로 추후 최적화/테스트에 유리합니다.

src/main/java/com/wayble/server/direction/service/TransportationService.java (4)

117-126: Node 프로젝션 변환 — 불변/필드 맵핑 적절

프로젝션 → 엔티티 변환이 명확합니다. 필요 컬럼만을 사용해 메모리 사용을 줄이는 방향이 좋아 보입니다.


127-149: Edge 프로젝션으로 Node 생성 시 nodeType 설정 검증 필요

startNode/endNode를 만들 때 row.getEdgeType()을 Node의 nodeType으로 채우고 있습니다. Node 자체의 타입 의미(정류장 종류)와 Edge 타입(BUS/SUBWAY/WALK)이 다르면 데이터 의미 왜곡이 생길 수 있습니다. 의도된 설계인지 확인이 필요합니다.

의도한 바가 정류장의 타입이 아닌 “해당 엣지로 접근한 모드”라면 문제없지만, Node의 고유 타입이라면 다음과 같이 수정이 필요할 수 있습니다(실제 프로젝션 필드에 맞게 조정):

- Node startNode = Node.createNode(
-     row.getStartNodeId(),
-     row.getStartStationName(),
-     row.getEdgeType(),              // edgeType → nodeType 재검토
-     row.getStartLatitude(),
-     row.getStartLongitude()
- );
+ Node startNode = Node.createNode(
+     row.getStartNodeId(),
+     row.getStartStationName(),
+     row.getStartNodeType(),         // 예시: 시작 노드 타입 필드가 있다면 사용
+     row.getStartLatitude(),
+     row.getStartLongitude()
+ );

필드 유무 확인이 필요하면 알려 주세요. 제가 리팩터링 패치 준비하겠습니다.


394-416: 경로 필터/정렬 로직은 합리적 — 단, 도보거리 산정 개선 필요

Transfer 우선, 도보거리 차순은 직관적입니다. 아래 calculateWalkDistance가 현재 상수 500m를 누적하고 있어 실제 거리 기반 정렬이 되지 않습니다. 이어지는 코멘트의 수정을 반영해 주세요.


69-99: 페이징(nextCursor) 오프셋 설계 — 현재 구현은 직관적

endIndex 기반 커서 산정은 단순/명확합니다. 추후 안정성을 위해 요청 파라미터와 함께 반환하는 커서 검증(예: 요청 커서 > 총 길이) 시 400 처리 여부만 검토하면 좋겠습니다.

Comment on lines +50 to +52
private NodeRef toNodeRef(Node node) {
return new NodeRef(node.getId(), node.getStationName(), node.getLatitude(), node.getLongitude());
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

toNodeRef NPE 가능성 — null-safe 대응 필요

mergeConsecutiveRoutes에서 walkStartNode/walkEndNode가 null일 가능성을 배제하지 못합니다. toNodeRef가 null을 그대로 처리하도록 방어해 주세요.

다음 패치를 제안합니다:

-    private NodeRef toNodeRef(Node node) {
-        return new NodeRef(node.getId(), node.getStationName(), node.getLatitude(), node.getLongitude());
-    }
+    private NodeRef toNodeRef(Node node) {
+        if (node == null) return null;
+        return new NodeRef(node.getId(), node.getStationName(), node.getLatitude(), node.getLongitude());
+    }
📝 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
private NodeRef toNodeRef(Node node) {
return new NodeRef(node.getId(), node.getStationName(), node.getLatitude(), node.getLongitude());
}
private NodeRef toNodeRef(Node node) {
if (node == null) return null;
return new NodeRef(node.getId(), node.getStationName(), node.getLatitude(), node.getLongitude());
}
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/direction/service/TransportationService.java
around lines 50 to 52, toNodeRef currently dereferences the Node and can throw
an NPE when called with null (e.g., mergeConsecutiveRoutes may pass
walkStartNode/walkEndNode as null); make the method null-safe by checking if
node == null and returning null (or Optional<NodeRef> if you prefer API change),
adjust the method signature/return type as needed, and update all call sites to
handle a possible null/empty result instead of assuming a non-null NodeRef.

@hyoinYang hyoinYang merged commit 1305b1c into develop Aug 21, 2025
1 check passed
@seung-in-Yoo
Copy link
Member

수고하셨습니다!! 그러면 저희 테스트만 더미 데이터를 사용하는건가요 아니면 대중교통 길찾기 자체에서도 더미 데이터로 사용하는건가요??

기능 자체(대중교통 길찾기의 버스 정보 출력)에서 더미 데이터를 사용합니다...! 일일트래픽 한도 때문에 더이상 테스트가 불가능한 상황이라 내일 데모데이 때 정보가 안 뜨는 것보단 더미데이터라도 사용하는 게 안정적이라고 생각했습니다...🥲

ㅠㅠ 열심히 데이터 찾고 하셨는데 효인님이 제일 아쉬울거같네요 그래도 데모데이 이후에 실제 데이터 넣어서 제대로 구현하면 더 좋겠네요! 너무 수고하셨습니다!!

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

Labels

🔧 refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants