Skip to content

Conversation

@hyoinYang
Copy link
Contributor

@hyoinYang hyoinYang commented Aug 13, 2025

✔️ 연관 이슈

📝 작업 내용

현재까지의 진행상황을 main으로 머지

스크린샷 (선택)

Summary by CodeRabbit

  • New Features

    • 대중교통 길찾기 응답 확장: 이동 구간 세부정보, 노선명/환승 정보, 버스(저상 여부·셔틀 여부·배차간격) 및 지하철 접근성(휠체어·엘리베이터·무장애 화장실) 제공
    • 외부 버스 도착/정류장 검색 연동으로 더 정확한 경로 안내
  • Performance

    • 데이터 로딩 및 페치 전략 최적화로 응답 속도와 안정성 향상
  • Chores

    • HTTP 클라이언트 연결·요청 타임아웃 설정 지원
    • 외부 데이터 연동을 위한 설정 항목 추가

@hyoinYang hyoinYang self-assigned this Aug 13, 2025
@hyoinYang hyoinYang added the release 프로덕션 서버에 배포 label Aug 13, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 13, 2025

Walkthrough

OpenData 연동을 위한 설정/DTO/서비스를 추가하고, 대중교통 길찾기 로직을 그래프 기반으로 전면 개편했습니다. 엔티티 지연 로딩과 페치 전략, 리포지토리 최적화를 도입했습니다. 응답 DTO 구조를 이동하고 확장했으며, 컨트롤러/서비스의 import를 따라 정리했습니다.

Changes

Cohort / File(s) Summary
애플리케이션 설정 등록
src/main/java/.../ServerApplication.java
OpenDataProperties를 @EnableConfigurationProperties에 추가
HTTP 클라이언트 설정
src/main/java/.../common/config/HttpClientConfig.java
java.net.http.HttpClient 빈과 요청 타임아웃(Duration) 빈 추가; 프로퍼티 바인딩
OpenData 설정/DTO
.../external/opendata/OpenDataProperties.java, .../external/opendata/dto/Arrival.java, .../external/opendata/dto/OpenDataResponse.java, .../external/opendata/dto/StationSearchResponse.java
OpenData API 프로퍼티 및 응답 매핑 DTO 신규 추가
컨트롤러 정리
.../direction/controller/TransportationController.java
요청/응답 DTO 경로 변경에 따른 import 수정
DTO 구조 개편
.../direction/dto/TransportationGraphDto.java, .../direction/dto/request/TransportationRequestDto.java, .../direction/dto/response/TransportationResponseDto.java, .../direction/dto/TransportationResponseDto.java(삭제), .../direction/dto/response/BusInfo.java
그래프 DTO 추가, 요청 DTO 패키지 이동, 응답 DTO를 response 패키지로 이동하며 구조 확장, 구 버전 삭제, BusInfo 별도 레코드 추가
엔티티 지연 로딩/구조 변경
.../entity/transportation/Edge.java, .../entity/transportation/Facility.java, .../entity/transportation/Node.java
연관관계 LAZY로 변경, Node의 OneToOne Facility 제거, 배치사이즈 추가, Node.createNode 정적 팩토리 추가
리포지토리
.../direction/repository/EdgeRepository.java, .../direction/repository/RouteRepository.java
Edge 페치 조인 메서드 추가 및 @repository 지정, Route JPA 리포지토리 신설
서비스
.../direction/service/BusInfoService.java, .../direction/service/TransportationService.java, .../direction/service/FacilityService.java
OpenData 기반 버스 정보 수집 서비스 추가, 길찾기 로직을 그래프+Dijkstra로 전면 개편 및 버스/지하철 정보 통합, DTO import 경로 수정

Sequence Diagram(s)

sequenceDiagram
  participant C as Client
  participant TC as TransportationController
  participant TS as TransportationService
  participant ER as EdgeRepository
  participant NR as NodeRepository
  participant BIS as BusInfoService
  participant ODS as OpenData API

  C->>TC: POST /directions
  TC->>TS: findDirections(request)
  TS->>NR: 조회: 노드/인접 노드
  TS->>ER: 조회: 엣지(페치 조인)
  TS->>TS: 그래프 구성 + 도보 엣지 추가
  TS->>TS: Dijkstra 실행(패널티/제약 적용)
  TS->>BIS: getBusInfo(stationName, busId, x, y)
  BIS->>ODS: 역 검색 API 호출
  ODS-->>BIS: 역 리스트
  BIS->>ODS: 도착 정보 API 호출
  ODS-->>BIS: 도착 정보
  BIS-->>TS: BusInfo
  TS-->>TC: TransportationResponseDto
  TC-->>C: 응답
Loading
sequenceDiagram
  participant BIS as BusInfoService
  participant HC as HttpClient
  participant ODP as OpenDataProperties
  participant OM as ObjectMapper
  participant RR as RouteRepository
  participant ODS as OpenData API

  BIS->>RR: findById(busId)
  RR-->>BIS: Route(Optional)
  BIS->>ODP: 엔드포인트/키/타임아웃 조회
  BIS->>HC: 역 이름 검색 요청 전송
  HC-->>BIS: JSON
  BIS->>OM: 역 검색 역직렬화
  BIS->>BIS: 최근접 역 선택
  BIS->>HC: 도착 정보 요청 전송
  HC-->>BIS: JSON
  BIS->>OM: 도착 정보 역직렬화
  BIS-->>BIS: 저상/배차간격 파싱
  BIS-->>Caller: TransportationResponseDto.BusInfo
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Assessment against linked issues

Objective Addressed Explanation
버스 저상 여부 실시간 반영, 버스 정보(시내/마을·마포) 표시, 이동 개수·역 정보 DTO 반영 (#124)
환승 패널티 적용 (#139)
최대 환승 4회 제한 (#139)
중간 도보 edge 동적 추가 및 도보 거리 제한(약 1000m) (#139)
N+1 문제 해결(지연 로딩/페치 최적화) (#139)
함수명 직관화 및 주석 추가 (#139) 코드에 일부 헬퍼 메서드 추가는 확인되나 주석/명명 기준 충족 여부는 불명확.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
별도 BusInfo DTO 레코드 추가 (src/main/java/com/wayble/server/direction/dto/response/BusInfo.java) 응답 DTO 내부에 중첩 BusInfo가 이미 정의되어 있어 중복 구조로 보이며, 명시된 요구사항에 독립 BusInfo 타입 추가는 없음. 실제 사용 여부가 불분명.
Node의 Facility 연관 제거 (src/main/java/com/wayble/server/direction/entity/transportation/Node.java) N+1 최적화 범주일 수 있으나, 요구사항에 연관 자체 제거는 명시되지 않음. 도메인 영향 가능성 있어 범위 초과일 수 있음(확신 낮음).

Possibly related PRs

Suggested labels

💡 feature, 🛠️ fix

Suggested reviewers

  • zyovn
  • seung-in-Yoo
  • KiSeungMin

Poem

토끼는 길을 짜-악, 그래프에 길을 깔고,
버스 온다! 저상인가? 귀 쫑긋 열고 살펴보고. 🚌
마포 바람 따라 환승은 가볍게, 패널티 톡-
도보로 살짝 잇고, Dijkstra는 쭉-
오늘도 한 번에 척! 길찾기 완주! 🥕

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

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.

@hyoinYang hyoinYang merged commit c31f0d4 into main Aug 13, 2025
2 of 3 checks passed
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: 9

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

262-362: 다익스트라 알고리즘 메서드가 너무 복잡합니다

runDijkstra 메서드가 100줄이 넘고 여러 책임을 가지고 있습니다. 동적 도보 연결 생성, 기존 엣지 처리, 경로 역추적 등을 별도 메서드로 분리하면 가독성과 유지보수성이 향상됩니다.

다음과 같이 리팩토링을 제안합니다:

 private List<TransportationResponseDto.Step> runDijkstra(Map<Long, List<Edge>> graph, Node start, Node end, Map<Pair<Long, Long>, Integer> weightMap, List<Node> nodes) {
     // 1. 초기화
-    Map<Long, Integer> distance = new HashMap<>();
-    Map<Long, Edge> prevEdge = new HashMap<>();
-    Map<Long, Node> prevNode = new HashMap<>();
-    Set<Long> visited = new HashSet<>();
-    
-    for (Node node : nodes) {
-        distance.put(node.getId(), Integer.MAX_VALUE);
-        prevNode.put(node.getId(), null);
-        prevEdge.put(node.getId(), null);
-    }
-    distance.put(start.getId(), 0);
+    DijkstraState state = initializeDijkstraState(nodes, start);
     
-    PriorityQueue<Node> pq = new PriorityQueue<>(Comparator.comparingInt(n -> distance.get(n.getId())));
+    PriorityQueue<Node> pq = new PriorityQueue<>(Comparator.comparingInt(n -> state.distance.get(n.getId())));
     pq.add(start);
     
     int visitedCount = 0;
     
     // 2. 다익스트라 알고리즘 실행
     while (!pq.isEmpty() && visitedCount < MAX_DIJKSTRA_VISITS) {
         Node curr = pq.poll();
         visitedCount++;
         
-        if (visited.contains(curr.getId())) continue;
-        visited.add(curr.getId());
+        if (state.visited.contains(curr.getId())) continue;
+        state.visited.add(curr.getId());
         
         if (curr.equals(end)) break;
         
-        List<Edge> currentEdges = graph.getOrDefault(curr.getId(), List.of());
-        
-        // 3. 동적 도보 연결 생성 (필요시)
-        boolean hasUnvisitedDirectConnection = false;
-        for (Edge edge : currentEdges) {
-            if (edge == null || edge.getEndNode() == null) continue;
-            Node neighbor = edge.getEndNode();
-            if (!visited.contains(neighbor.getId())) {
-                hasUnvisitedDirectConnection = true;
-                break;
-            }
-        }
-        
-        if (!hasUnvisitedDirectConnection) {
-            List<Node> nearbyNodes = findNearbyNodes(nodes, curr.getLatitude(), curr.getLongitude(), NEARBY_STATION_WALK_DISTANCE);
-            for (Node nearbyNode : nearbyNodes) {
-                if (visited.contains(nearbyNode.getId())) continue;
-                
-                double walkDistance = haversine(
-                        curr.getLatitude(), curr.getLongitude(),
-                        nearbyNode.getLatitude(), nearbyNode.getLongitude()
-                ) * METER_CONVERSION;
-                
-                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);
-                    }
-                }
-            }
-        }
+        List<Edge> currentEdges = new ArrayList<>(graph.getOrDefault(curr.getId(), List.of()));
+        
+        // 3. 동적 도보 연결 추가
+        addDynamicWalkEdges(currentEdges, curr, nodes, state, pq);
         
         // 4. 기존 엣지 처리
-        for (Edge edge : currentEdges) {
-            if (edge == null || edge.getEndNode() == null) continue;
-            
-            Node neighbor = edge.getEndNode();
-            if (visited.contains(neighbor.getId())) continue;
-            
-            if (edge.getStartNode() == null || edge.getEndNode() == null ||
-                    edge.getStartNode().getId() == null || edge.getEndNode().getId() == null) {
-                continue;
-            }
-            
-            Pair<Long, Long> key = Pair.of(edge.getStartNode().getId(), edge.getEndNode().getId());
-            int baseWeight = weightMap.getOrDefault(key,
-                    (int)(haversine(
-                            edge.getStartNode().getLatitude(), edge.getStartNode().getLongitude(),
-                            edge.getEndNode().getLatitude(), edge.getEndNode().getLongitude()
-                    ) * METER_CONVERSION)
-            );
-            
-            int weight = baseWeight;
-            
-            // 환승 패널티 적용
-            Edge prevEdgeForCurr = prevEdge.get(curr.getId());
-            if (prevEdgeForCurr != null &&
-                    prevEdgeForCurr.getEdgeType() != DirectionType.WALK &&
-                    edge.getEdgeType() != DirectionType.WALK) {
-                
-                if (prevEdgeForCurr.getEdgeType() != edge.getEdgeType()) {
-                    weight += TRANSFER_PENALTY;
-                } else {
-                    Route prevRoute = prevEdgeForCurr.getRoute();
-                    Route currentRoute = edge.getRoute();
-                    
-                    if (prevRoute != null && currentRoute != null && 
-                        !prevRoute.getRouteId().equals(currentRoute.getRouteId())) {
-                        weight += TRANSFER_PENALTY;
-                    }
-                }
-            }
-            
-            weight += STEP_PENALTY;
-            
-            int alt = distance.get(curr.getId()) + weight;
-            if (alt < distance.get(neighbor.getId())) {
-                distance.put(neighbor.getId(), alt);
-                prevNode.put(neighbor.getId(), curr);
-                prevEdge.put(neighbor.getId(), edge);
-                pq.add(neighbor);
-            }
-        }
+        processEdges(currentEdges, curr, state, weightMap, pq);
     }
     
     // 5. 경로 역추적 및 steps 생성
-    if (distance.get(end.getId()) == Integer.MAX_VALUE) {
-        return new LinkedList<>();
-    }
-    
-    List<Edge> pathEdges = new ArrayList<>();
-    Node current = end;
-    Set<Long> backtrackVisited = new HashSet<>();
-    
-    while (current != null && !current.equals(start)) {
-        if (backtrackVisited.contains(current.getId())) break;
-        backtrackVisited.add(current.getId());
-        
-        Edge edge = prevEdge.get(current.getId());
-        if (edge == null) break;
-        
-        pathEdges.add(0, edge);
-        current = prevNode.get(current.getId());
-    }
+    List<Edge> pathEdges = reconstructPath(start, end, state);
     
     return mergeConsecutiveRoutes(pathEdges);
 }
+
+private static class DijkstraState {
+    Map<Long, Integer> distance;
+    Map<Long, Edge> prevEdge;
+    Map<Long, Node> prevNode;
+    Set<Long> visited;
+}
🧹 Nitpick comments (22)
src/main/java/com/wayble/server/direction/entity/transportation/Node.java (5)

37-39: 컬렉션 LAZY + BatchSize 적용 적합

outgoingEdges를 LAZY로 두고 배치 로딩 설정한 점 좋습니다. 다만 비영속(수동 new) 객체에서 이 컬렉션을 접근할 가능성이 있으면 NPE를 피하기 위해 기본 초기화를 고려해 주세요.

다음 변경으로 안전성을 높일 수 있습니다:

-    private List<Edge> outgoingEdges;
+    private List<Edge> outgoingEdges = new ArrayList<>();

추가로 import가 필요합니다:

import java.util.ArrayList;

42-44: 컬렉션 LAZY + BatchSize 적용 적합

incomingEdges 역시 동일 제안 적용 가능합니다.

-    private List<Edge> incomingEdges;
+    private List<Edge> incomingEdges = new ArrayList<>();

47-49: 컬렉션 LAZY + BatchSize 적용 적합

startRoutes도 비영속 사용 시 NPE 예방을 위해 기본 초기화를 권장합니다.

-    private List<Route> startRoutes;
+    private List<Route> startRoutes = new ArrayList<>();

51-53: 컬렉션 LAZY + BatchSize 적용 적합

endRoutes도 동일합니다.

-    private List<Route> endRoutes;
+    private List<Route> endRoutes = new ArrayList<>();

63-65: 정적 팩토리 추가 LGTM + 비영속 사용 시 컬렉션 초기화 고려

createNode 도입은 명시적 생성을 유도해 가독성에 도움이 됩니다. 다만 본 팩토리로 생성되는 비영속 Node를 그래프 빌드 등에서 사용한다면 컬렉션 필드가 null일 수 있으므로 위 제안한 기본 초기화를 함께 적용하는 것을 권장합니다.

src/main/java/com/wayble/server/direction/external/opendata/OpenDataProperties.java (3)

5-14: 구성 프로퍼티 유효성 보강(@validated) 및 timeout 타입 Duration 권장

구성값 신뢰성을 높이기 위해 Bean Validation을 적용하고, 정수 기반 timeout 대신 Duration 바인딩을 권장합니다. Spring Boot는 Duration 바인딩을 네이티브로 지원합니다.

아래처럼 개선을 제안드립니다:

 package com.wayble.server.direction.external.opendata;

 import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Positive;
+import java.time.Duration;

-@ConfigurationProperties(prefix = "opendata.api")
+@ConfigurationProperties(prefix = "opendata.api")
+@Validated
 public record OpenDataProperties(
-        String key,
-        String baseUrl,
+        @NotBlank String key,
+        @NotBlank String baseUrl,
         String encodedKey,
-        Endpoints endpoints,
-        int timeout,
+        @Valid Endpoints endpoints,
+        @Positive Duration timeout,
         String userAgent,
         String accept
 ) {
     public record Endpoints(String arrivals, String stationByName) {}
 }

7-10: encodedKey 중복 보관 지양

key와 encodedKey를 동시 보관하면 설정 불일치/보안 리스크가 생길 수 있습니다. 런타임에서 필요 시 인코딩하도록 하고 구성에는 key만 두는 방식을 권장합니다.

원하시면 HttpClient/WebClient 레벨에서 자동 인코딩하도록 유틸을 제안드리겠습니다.


5-14: OpenData 전용 WebClient 구성 제안

tMap/kric과 동일하게 OpenData용 WebClient(+기본 헤더/타임아웃)를 별도 Bean으로 구성하면 헤더(userAgent/accept) 적용과 유지보수가 쉬워집니다.

WebClientConfig 예시:

@Bean
public WebClient openDataWebClient(OpenDataProperties properties) {
    return WebClient.builder()
            .baseUrl(properties.baseUrl())
            .defaultHeader(HttpHeaders.USER_AGENT, properties.userAgent())
            .defaultHeader(HttpHeaders.ACCEPT, properties.accept())
            .build();
}
src/main/java/com/wayble/server/direction/external/opendata/dto/StationSearchResponse.java (1)

6-12: MsgBody에도 ignoreUnknown 적용 및 itemList 명시적 바인딩 권장

외부 스키마 변동에 더 탄탄히 대응하려면 MsgBody에도 @JsonIgnoreProperties를, itemList에는 @JsonProperty를 추가하는 것을 권장합니다.

아래와 같이 보강 가능합니다:

 package com.wayble.server.direction.external.opendata.dto;

 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
 import java.util.List;

 @JsonIgnoreProperties(ignoreUnknown = true)
 public record StationSearchResponse( // 버스 정류장 id를 검색하는 엔드포인트
         StationSearchMsgBody msgBody
 ) {
-    public record StationSearchMsgBody(
-            List<StationItem> itemList
-    ) {}
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public record StationSearchMsgBody(
+            @JsonProperty("itemList") List<StationItem> itemList
+    ) {}
src/main/java/com/wayble/server/direction/external/opendata/dto/OpenDataResponse.java (3)

5-6: itemList 단일 객체 응답 허용 설정 권장

공공데이터 API가 itemList를 단일 객체로 반환하는 경우가 있어, Jackson의 ACCEPT_SINGLE_VALUE_AS_ARRAY를 적용하면 역직렬화 안정성이 높아집니다.

아래처럼 보강을 제안드립니다:

 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonFormat;

 ...
     public record MsgBody(
-        @JsonProperty("itemList") List<Item> itemList
+        @JsonProperty("itemList")
+        @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
+        List<Item> itemList
     ) {}

Also applies to: 28-30


14-26: 중첩 헤더 DTO에도 ignoreUnknown 적용

스키마 확장에 대비해 ComMsgHeader/MsgHeader에도 @JsonIgnoreProperties(ignoreUnknown = true)를 동일 적용하는 것을 권장합니다.

적용 예:

-    public record ComMsgHeader(
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public record ComMsgHeader(
         @JsonProperty("errMsg") String errMsg,
         @JsonProperty("responseTime") String responseTime,
         @JsonProperty("requestMsgID") String requestMsgID,
         @JsonProperty("responseMsgID") String responseMsgID,
         @JsonProperty("successYN") String successYN,
         @JsonProperty("returnCode") String returnCode
     ) {}
-    public record MsgHeader(
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public record MsgHeader(
         @JsonProperty("headerMsg") String headerMsg,
         @JsonProperty("headerCd") String headerCd,
         @JsonProperty("itemCount") Integer itemCount
     ) {}

34-36: term 타입(Number) 사용 검토

term(배차간격)이 항상 숫자라면 String 대신 Integer로 매핑하는 편이 파싱/검증에 유리합니다. 외부가 문자열로 반환한다면 현재 방식 유지 + 서비스 레이어에서 안전 파싱(NumberFormatException 방지)을 권장합니다.

API 응답 샘플을 확인해 실제 타입을 검증 부탁드립니다. 필요 시 서비스 레이어 파싱 로직 제안 가능합니다.

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

3-3: 중복 호출 완화 위해 캐싱 적용 고려

같은 stinCd에 대한 반복 호출이 잦다면 캐시(@Cacheable) 적용으로 외부 API 부하와 지연을 낮출 수 있습니다. TTL은 KRIC 데이터 갱신 주기에 맞춰 설정하세요.

원하시면 Cache 설정/애노테이션 적용 패치도 제안드리겠습니다.

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

10-16: N+1 방지를 위한 fetch join은 적절합니다. 다만 “전체 Edge 일괄 로딩”의 비용을 관리할 방법을 함께 준비하세요.

  • DISTINCT + JOIN FETCH(startNode, endNode) + LEFT JOIN FETCH(route) 구성은 의도대로 연관 로딩을 1쿼리로 해결합니다.
  • 그래프 구축 시 Edge 전량을 항상 적재하면 데이터가 커질수록 메모리/응답시간 비용이 커집니다. 다음을 고려해보세요:
    • 조건/범위 기반 조회(예: 특정 도시/노선/타입별) 메서드 추가
    • 페이징/스트리밍 조회 도입
    • 자주 쓰는 연관 로딩은 @entitygraph로 대체하여 쿼리 가독성 향상
  • DB 인덱스(start_node_id, end_node_id, route_id)가 없다면 조인 성능 확보를 위해 생성 권장.
src/main/java/com/wayble/server/common/config/HttpClientConfig.java (2)

3-3: 불필요한 Lombok @requiredargsconstructor 제거 제안

필드 주입(@value)만 사용하고 있어 생성자가 필요 없습니다. import와 어노테이션을 정리하세요.

-import lombok.RequiredArgsConstructor;
@@
-@RequiredArgsConstructor

Also applies to: 11-11


14-19: 원시 int + 초 단위보다는 Duration 기반의 @ConfigurationProperties 사용을 권장합니다.

  • 가독성과 타입 안정성을 위해 Duration으로 직접 바인딩하세요.
  • 팀 내 일관성(TMapProperties, KricProperties와 동일한 패턴)도 좋아집니다.

예시: 새로운 프로퍼티 홀더 추가

// src/main/java/com/wayble/server/common/config/HttpClientProperties.java
@ConfigurationProperties(prefix = "http.client")
public record HttpClientProperties(
    Duration connectTimeout,
    Duration requestTimeout
) {}

적용: 구성 클래스에서 @value 제거하고 프로퍼티 주입

@Configuration
@EnableConfigurationProperties(HttpClientProperties.class)
public class HttpClientConfig {

    private final HttpClientProperties props;

    public HttpClientConfig(HttpClientProperties props) {
        this.props = props;
    }

    @Bean
    public HttpClient httpClient() {
        return HttpClient.newBuilder()
                .connectTimeout(props.connectTimeout())
                .build();
    }

    @Bean
    public Duration httpRequestTimeout() {
        return props.requestTimeout();
    }
}

ServerApplication의 @EnableConfigurationProperties에 HttpClientProperties를 추가하는 방식 대신, 위 예시처럼 구성 클래스에 EnableConfigurationProperties를 붙이면 전역 변경 없이 국소 적용이 가능합니다.

src/main/java/com/wayble/server/direction/entity/transportation/Edge.java (1)

24-31: start/end 노드가 필수라면 optional=false로 제약을 명확히 하세요.

DB/도메인 규칙상 startNode, endNode가 항상 있어야 한다면 JPA 매핑에도 반영하는 것이 안전합니다.

-    @ManyToOne(fetch = FetchType.LAZY)
+    @ManyToOne(fetch = FetchType.LAZY, optional = false)
     @JoinColumn(name = "start_node_id")
     private Node startNode;

-    @ManyToOne(fetch = FetchType.LAZY)
+    @ManyToOne(fetch = FetchType.LAZY, optional = false)
     @JoinColumn(name = "end_node_id")
     private Node endNode;

참고: 데이터베이스 제약도 일치시키려면 @joincolumn의 nullable=false 지정도 함께 고려하세요.

src/main/java/com/wayble/server/direction/dto/TransportationGraphDto.java (1)

11-11: Pair 대신 전용 Edge 키 클래스 고려

Map<Pair<Long, Long>, Integer>에서 Pair를 사용하는 것은 동작하지만, 도메인 특화 키 클래스(예: EdgeKey)를 만들면 코드의 의도가 더 명확해지고 타입 안전성이 향상됩니다.

다음과 같은 전용 레코드를 추가하는 것을 고려해보세요:

public record EdgeKey(Long fromNodeId, Long toNodeId) {}

그리고 weightMap의 타입을 다음과 같이 변경:

-    Map<Pair<Long, Long>, Integer> weightMap // 엣지별 가중치
+    Map<EdgeKey, Integer> weightMap // 엣지별 가중치
src/main/java/com/wayble/server/direction/service/BusInfoService.java (1)

188-188: 거리 계산 최적화 가능

유클리드 거리 계산에서 제곱근을 구할 필요가 없습니다. 최소값만 찾으면 되므로 제곱 거리로도 충분합니다.

성능 향상을 위해 다음과 같이 수정하세요:

-double distance = Math.sqrt(Math.pow(stationX - x, 2) + Math.pow(stationY - y, 2));
+double distanceSquared = Math.pow(stationX - x, 2) + Math.pow(stationY - y, 2);

-if (distance < minDistance) {
-    minDistance = distance;
+if (distanceSquared < minDistance) {
+    minDistance = distanceSquared;
     closestStation = station;
 }
src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java (2)

34-38: BusInfo의 isLowFloor 필드 타입 재고려

List<Boolean> 대신 더 명확한 타입을 사용하면 의미가 분명해집니다. 현재는 리스트의 각 원소가 무엇을 나타내는지(busType1, busType2) 불명확합니다.

다음과 같은 구조를 고려해보세요:

 public record BusInfo(
         boolean isShuttleBus, // routeName에 "마포" 포함시 true
-        @Nullable List<Boolean> isLowFloor, // Open API(busType1,busType2) 기반 저상 여부 리스트
+        @Nullable LowFloorInfo lowFloorInfo, // Open API 기반 저상 버스 정보
         @Nullable Integer dispatchInterval // Open API(term) 기반 배차간격
 ){}
+
+public record LowFloorInfo(
+        boolean firstBusLowFloor,  // busType1 == "1"
+        boolean secondBusLowFloor  // busType2 == "1"
+){}

51-56: 중복된 NodeInfo 정의

NodeInfo가 SubwayInfo와 매우 유사한 구조를 가지고 있습니다. 서비스 내부용이라는 주석이 있지만, SubwayInfo를 재사용하거나 상속 관계를 고려해보세요.

NodeInfo를 제거하고 SubwayInfo를 사용하거나, 공통 인터페이스를 도입하세요:

-// 지하철 시설 정보 묶음 (서비스 내부에서 사용)
-public record NodeInfo(
-        List<LocationInfo> wheelchair,
-        List<LocationInfo> elevator,
-        Boolean accessibleRestroom
-) {}
+// NodeInfo는 SubwayInfo의 별칭으로 사용
+public static class NodeInfo extends SubwayInfo {
+    public NodeInfo(List<LocationInfo> wheelchair, List<LocationInfo> elevator, Boolean accessibleRestroom) {
+        super(wheelchair, elevator, accessibleRestroom);
+    }
+}
src/main/java/com/wayble/server/direction/service/TransportationService.java (1)

137-140: 매직넘버를 상수로 정의

환승 횟수 제한값 4가 하드코딩되어 있습니다. 상수로 정의하면 유지보수가 용이해집니다.

상수를 추가하세요:

+private static final int MAX_TRANSFER_COUNT = 4; // 최대 환승 횟수

 // 4. 환승 횟수 검증 (4회 이상 제외)
 int transferCount = calculateTransferCount(route);
-if (transferCount >= 4) {
+if (transferCount >= MAX_TRANSFER_COUNT) {
     return new ArrayList<>();
 }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between d3ba937 and a6aa693.

📒 Files selected for processing (20)
  • src/main/java/com/wayble/server/ServerApplication.java (2 hunks)
  • src/main/java/com/wayble/server/common/config/HttpClientConfig.java (1 hunks)
  • src/main/java/com/wayble/server/direction/controller/TransportationController.java (1 hunks)
  • src/main/java/com/wayble/server/direction/dto/TransportationGraphDto.java (1 hunks)
  • src/main/java/com/wayble/server/direction/dto/TransportationResponseDto.java (0 hunks)
  • src/main/java/com/wayble/server/direction/dto/request/TransportationRequestDto.java (1 hunks)
  • src/main/java/com/wayble/server/direction/dto/response/BusInfo.java (1 hunks)
  • src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.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/Facility.java (1 hunks)
  • src/main/java/com/wayble/server/direction/entity/transportation/Node.java (3 hunks)
  • src/main/java/com/wayble/server/direction/external/opendata/OpenDataProperties.java (1 hunks)
  • src/main/java/com/wayble/server/direction/external/opendata/dto/Arrival.java (1 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/RouteRepository.java (1 hunks)
  • src/main/java/com/wayble/server/direction/service/BusInfoService.java (1 hunks)
  • src/main/java/com/wayble/server/direction/service/FacilityService.java (1 hunks)
  • src/main/java/com/wayble/server/direction/service/TransportationService.java (7 hunks)
💤 Files with no reviewable changes (1)
  • src/main/java/com/wayble/server/direction/dto/TransportationResponseDto.java
🧰 Additional context used
🧬 Code Graph Analysis (14)
src/main/java/com/wayble/server/direction/entity/transportation/Facility.java (4)
src/main/java/com/wayble/server/direction/entity/transportation/Elevator.java (1)
  • Entity (6-26)
src/main/java/com/wayble/server/direction/entity/transportation/Lift.java (1)
  • Entity (6-29)
src/main/java/com/wayble/server/direction/repository/FacilityRepository.java (2)
  • FacilityRepository (10-12)
  • findByNodeId (11-11)
src/main/java/com/wayble/server/wayblezone/entity/WaybleZoneFacility.java (1)
  • Entity (8-57)
src/main/java/com/wayble/server/direction/dto/response/BusInfo.java (3)
src/main/java/com/wayble/server/direction/dto/TransportationRequestDto.java (1)
  • Schema (5-17)
src/main/java/com/wayble/server/wayblezone/dto/WaybleZoneDetailResponseDto.java (1)
  • Builder (9-28)
src/main/java/com/wayble/server/direction/dto/response/WayblePathResponse.java (1)
  • Builder (9-49)
src/main/java/com/wayble/server/ServerApplication.java (3)
src/main/java/com/wayble/server/common/config/WebClientConfig.java (1)
  • Configuration (10-36)
src/main/java/com/wayble/server/common/client/tmap/TMapProperties.java (1)
  • ConfigurationProperties (5-10)
src/main/java/com/wayble/server/direction/external/kric/KricProperties.java (1)
  • ConfigurationProperties (5-10)
src/main/java/com/wayble/server/common/config/HttpClientConfig.java (5)
src/main/java/com/wayble/server/common/config/WebClientConfig.java (4)
  • Configuration (10-36)
  • Bean (17-21)
  • Bean (30-35)
  • Bean (23-28)
src/main/java/com/wayble/server/common/config/ElasticsearchConfig.java (2)
  • Configuration (8-22)
  • Override (14-21)
src/main/java/com/wayble/server/common/config/SwaggerConfig.java (1)
  • Configuration (12-34)
src/main/java/com/wayble/server/logging/config/LoggingAsyncConfig.java (1)
  • Configuration (10-26)
src/main/java/com/wayble/server/common/config/security/JwtProperties.java (1)
  • Component (6-34)
src/main/java/com/wayble/server/direction/controller/TransportationController.java (2)
src/main/java/com/wayble/server/direction/dto/TransportationRequestDto.java (2)
  • Schema (5-17)
  • Location (12-16)
src/main/java/com/wayble/server/direction/dto/TransportationResponseDto.java (2)
  • Schema (9-37)
  • Step (14-20)
src/main/java/com/wayble/server/direction/external/opendata/OpenDataProperties.java (3)
src/main/java/com/wayble/server/direction/external/kric/KricProperties.java (1)
  • ConfigurationProperties (5-10)
src/main/java/com/wayble/server/common/client/tmap/TMapProperties.java (1)
  • ConfigurationProperties (5-10)
src/main/java/com/wayble/server/common/config/WebClientConfig.java (1)
  • Configuration (10-36)
src/main/java/com/wayble/server/direction/service/FacilityService.java (1)
src/main/java/com/wayble/server/direction/dto/TransportationResponseDto.java (1)
  • Schema (9-37)
src/main/java/com/wayble/server/direction/repository/EdgeRepository.java (1)
src/main/java/com/wayble/server/direction/entity/transportation/Route.java (1)
  • Entity (9-36)
src/main/java/com/wayble/server/direction/external/opendata/dto/OpenDataResponse.java (6)
src/main/java/com/wayble/server/direction/dto/TransportationResponseDto.java (1)
  • Schema (9-37)
src/main/java/com/wayble/server/common/client/tmap/dto/response/TMapResponse.java (1)
  • TMapResponse (5-43)
src/main/java/com/wayble/server/review/dto/ReviewResponseDto.java (1)
  • ReviewResponseDto (6-14)
src/main/java/com/wayble/server/common/client/tmap/dto/response/TMapParsingResponse.java (2)
  • Builder (8-74)
  • Schema (24-64)
src/main/java/com/wayble/server/direction/dto/TransportationRequestDto.java (1)
  • Schema (5-17)
src/main/java/com/wayble/server/wayblezone/dto/WaybleZoneDetailResponseDto.java (1)
  • Builder (9-28)
src/main/java/com/wayble/server/direction/dto/TransportationGraphDto.java (1)
src/main/java/com/wayble/server/direction/entity/Edge.java (1)
  • Edge (5-11)
src/main/java/com/wayble/server/direction/entity/transportation/Node.java (2)
src/main/java/com/wayble/server/direction/entity/transportation/Route.java (1)
  • Entity (9-36)
src/main/java/com/wayble/server/direction/repository/NodeRepository.java (1)
  • NodeRepository (6-7)
src/main/java/com/wayble/server/direction/dto/request/TransportationRequestDto.java (1)
src/main/java/com/wayble/server/direction/dto/TransportationRequestDto.java (2)
  • Schema (5-17)
  • Location (12-16)
src/main/java/com/wayble/server/direction/external/opendata/dto/StationSearchResponse.java (4)
src/main/java/com/wayble/server/direction/dto/TransportationResponseDto.java (4)
  • Schema (9-37)
  • Step (14-20)
  • NodeInfo (27-31)
  • LocationInfo (33-36)
src/main/java/com/wayble/server/direction/dto/TransportationRequestDto.java (1)
  • Schema (5-17)
src/main/java/com/wayble/server/common/client/tmap/dto/response/TMapParsingResponse.java (1)
  • Builder (8-74)
src/main/java/com/wayble/server/admin/dto/wayblezone/AdminWaybleZoneDetailDto.java (1)
  • AdminWaybleZoneDetailDto (10-59)
src/main/java/com/wayble/server/direction/service/TransportationService.java (3)
src/main/java/com/wayble/server/direction/service/BusInfoService.java (1)
  • Service (22-203)
src/main/java/com/wayble/server/direction/service/FacilityService.java (1)
  • Service (23-109)
src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java (1)
  • Service (16-170)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: run-docker-image-on-ec2
🔇 Additional comments (10)
src/main/java/com/wayble/server/direction/dto/request/TransportationRequestDto.java (1)

1-1: 패키지 이동 영향 없음 확인됨

검증 결과, 기존 패키지( com.wayble.server.direction.dto )에 대한 import나 FQCN 직접 참조가 없으며, TransportationRequestDto.java 파일도 새 위치(src/main/java/com/wayble/server/direction/dto/request/)에만 존재합니다. 추가 조치가 필요하지 않습니다.

src/main/java/com/wayble/server/direction/entity/transportation/Node.java (1)

9-10: BatchSize 도입 LGTM

컬렉션 로딩 시 배치 크기 지정으로 N+1 완화에 도움 됩니다.

src/main/java/com/wayble/server/direction/entity/transportation/Facility.java (1)

41-44: @OnetoOne LAZY 전환에 따른 LazyInitializationException 리스크 점검

@mapsid를 유지한 채 LAZY로 바꾸면 트랜잭션 밖에서 facility.getNode() 접근 시 LazyInitializationException 가능성이 커집니다. 접근 지점이 항상 트랜잭션 내부인지, 혹은 리포지토리 조회 시 fetch join/@entitygraph로 함께 가져오는지 확인해 주세요.

성능/안정성을 위해 다음을 점검하세요:

  • 노드와 항상 함께 필요한 조회는 JPQL fetch join 또는 Spring Data @entitygraph 사용
  • toString/로깅/직렬화 경로에서 node 접근이 이루어지지 않도록 주의
src/main/java/com/wayble/server/direction/repository/RouteRepository.java (1)

7-9: LGTM — 기본 JPA 리포지토리 정의 적절

필요 최소한의 설정으로 충분합니다. 추후 조회 패턴이 확정되면 메서드 시그니처(Derived Query)만 보강하면 되겠습니다.

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

3-3: 검증 결과: 구 패키지 참조 없음
ripgrep로 com.wayble.server.direction.dto.TransportationResponseDto를 검색한 결과, 코드베이스에 남아있는 참조가 없습니다.

src/main/java/com/wayble/server/ServerApplication.java (1)

5-5: OpenDataProperties 바인딩 활성화는 적절합니다. 프로퍼티 키 존재 여부만 확인해주세요.

  • OpenDataProperties를 EnableConfigurationProperties에 포함한 선택은 타당합니다.
  • 운영/스테이징 등 모든 프로필의 설정 파일에 opendata.api.* 키가 누락되지 않았는지 확인해주세요. (예: opendata.api.key, opendata.api.base-url, opendata.api.endpoints.*)

Also applies to: 20-20

src/main/java/com/wayble/server/direction/controller/TransportationController.java (1)

4-5: DTO 패키지 이동 반영 — 문제 없습니다.

import 경로 변경만으로 컨트롤러 로직에는 영향이 없습니다.

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

20-25: HttpClient에 연결 타임아웃만 적용되어 있습니다. 요청 타임아웃 적용 경로를 확인해주세요.

  • Java HttpClient는 소켓/읽기 타임아웃을 전역으로 두지 않고, 요청별(HttpRequest.Builder.timeout)로 설정합니다.
  • 제공된 Duration 빈(httpRequestTimeout)이 실제 HttpRequest 생성 시 적용되는지 BusInfoService 등 사용처를 점검해주세요.
src/main/java/com/wayble/server/direction/entity/transportation/Edge.java (1)

24-35: ManyToOne LAZY 전환은 좋습니다. LazyInitializationException 방지를 위해 서비스 계층의 fetch 전략을 일관되게 유지하세요.

  • EdgeRepository.findAllWithNodesAndRoute()처럼 fetch join 메서드로 접근하면 안전합니다.
  • 로깅/직렬화 시 지연 로딩 프록시 접근이 발생하지 않도록 엔티티를 직접 반환하지 말고 DTO로 변환하는 현재 흐름을 유지하세요.
src/main/java/com/wayble/server/direction/dto/TransportationGraphDto.java (1)

9-12: 레코드 필드를 final로 명시하지 않아도 됩니다

Java 레코드의 필드는 자동으로 private final이 되므로 별도로 명시할 필요가 없습니다. 현재 코드가 정상적으로 작동합니다.

Comment on lines +4 to +5
import com.wayble.server.direction.dto.request.TransportationRequestDto;
import com.wayble.server.direction.dto.response.TransportationResponseDto;
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

레거시 패키지 참조 잔재 여부 확인 권장

다른 코드에 기존 패키지(com.wayble.server.direction.dto.*)를 참조하는 부분이 남아있지 않은지 점검해주세요.

다음 스크립트로 레거시 import를 검색할 수 있습니다:


🏁 Script executed:

#!/bin/bash
# 레거시 DTO 패키지 참조 검색
rg -n "com\.wayble\.server\.direction\.dto\.Transportation(Request|Response)Dto"

Length of output: 82


추가 검증을 위해 레거시 DTO 패키지에 대한 모든 import 참조를 검색해 주세요.


🏁 Script executed:

#!/bin/bash
# 레거시 DTO 패키지 모든 import 참조 검색
rg -n '^import com\.wayble\.server\.direction\.dto'

Length of output: 3572


레거시 DTO 패키지 참조 업데이트 필요

아래 파일들에 아직 com.wayble.server.direction.dto.* 패키지 import가 남아있습니다. 모두 새로운 DTO 패키지 경로로 변경해주세요.

  • src/main/java/com/wayble/server/direction/service/TransportationService.java
  • src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java
  • src/main/java/com/wayble/server/direction/service/FacilityService.java
  • src/main/java/com/wayble/server/direction/service/BusInfoService.java
  • src/main/java/com/wayble/server/direction/service/WalkingService.java
  • src/main/java/com/wayble/server/direction/controller/TransportationController.java
  • src/main/java/com/wayble/server/direction/controller/swagger/WalkingSwagger.java
  • src/main/java/com/wayble/server/direction/controller/swagger/DirectionSwagger.java
  • src/main/java/com/wayble/server/direction/controller/WalkingController.java
  • src/main/java/com/wayble/server/direction/controller/DirectionController.java
  • src/main/java/com/wayble/server/direction/repository/DirectionElasticSearchCustomRepository.java
  • src/main/java/com/wayble/server/direction/repository/DirectionElasticSearchCustomRepositoryImpl.java
  • src/main/java/com/wayble/server/direction/service/DirectionService.java
  • src/test/java/com/wayble/server/direction/service/WalkingServiceTest.java

위 파일들에서 import com.wayble.server.direction.dto.*를 삭제하고, 새로 정의된 DTO 패키지(import 경로)를 적용해 주세요.

🤖 Prompt for AI Agents
In
src/main/java/com/wayble/server/direction/controller/TransportationController.java
around lines 4-5, remove the legacy imports importing
com.wayble.server.direction.dto.request.TransportationRequestDto and
com.wayble.server.direction.dto.response.TransportationResponseDto and replace
them with imports from the new DTO package used in the project (update to the
project's newly defined DTO import path for TransportationRequestDto and
TransportationResponseDto); apply the exact same replacement across all files
listed in the review so no references to com.wayble.server.direction.dto.*
remain.

Comment on lines +1 to +11
package com.wayble.server.direction.dto.response;

import java.util.List;

public record BusInfo(List<BusArrival> buses, String stationName) {
public record BusArrival(
String busNumber,
String arrival1,
String arrival2
) {}
}
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

동일 명칭 DTO 중복/혼선 위험: TransportationResponseDto.BusInfo와 충돌 가능

본 파일의 BusInfo와 TransportationResponseDto.BusInfo가 공존하면 import 충돌·오용 위험이 큽니다. 실제 서비스는 TransportationResponseDto.BusInfo를 반환하는 것으로 보이며, 이 파일이 미사용이라면 제거하거나 명확히 목적을 구분해 리네이밍(예: StationArrivalsResponse)하는 것을 권장합니다. 사용 중이라면 컨트롤러/서비스 반환 타입 정합성을 재검토해 주세요.

아래 스크립트로 본 BusInfo의 실제 사용 여부와 이름 충돌 가능성을 점검하세요.

API로 노출할 DTO가 둘 필요가 없다면 하나로 통일해 주세요. 둘 다 필요하면 명칭을 분리하고 @Schema 문서화를 추가해 혼선을 줄이는 것을 권장합니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "본 BusInfo(패키지 dto.response) 사용처:"
rg -n $'com\\.wayble\\.server\\.direction\\.dto\\.response\\.BusInfo' || true
rg -n $'\\bBusInfo\\b' | rg -n -v 'TransportationResponseDto' || true

echo
echo "TransportationResponseDto.BusInfo 사용처:"
rg -n $'TransportationResponseDto\\.BusInfo'

Length of output: 1694


미사용 BusInfo DTO 제거 또는 명칭 변경 필요

com.wayble.server.direction.dto.response.BusInfo는 정의된 곳 외에 코드베이스 어디에서도 참조되지 않습니다. 반면 실제 서비스 로직에서는 TransportationResponseDto.BusInfo만 사용 중이므로, 중복된 DTO를 유지할 이유가 없습니다. 아래 조치 중 하나를 선택해 반영해 주세요:

  • 불필요한 파일 삭제
    src/main/java/com/wayble/server/direction/dto/response/BusInfo.java
  • 동일 역할이 필요하다면 명칭을 명확히 구분하고 문서화
    • 예: StationArrivalsResponse 등으로 리네이밍 후 @Schema 어노테이션 추가
🤖 Prompt for AI Agents
src/main/java/com/wayble/server/direction/dto/response/BusInfo.java (lines
1-11): 이 DTO는 코드베이스에서 사용되지 않으므로 파일을 삭제하거나, 동일한 역할이 필요하면 명확한 명칭으로 리네임하고 문서화해야
합니다; 선택 1 — 불필요하면 파일을
삭제(src/main/java/com/wayble/server/direction/dto/response/BusInfo.java)하고 관련
import/참조가 없는지 확인해 커밋하세요; 선택 2 — 필요하면 클래스명을 예: StationArrivalsResponse로 변경하고 적절한
@Schema 주석과 설명을 추가한 뒤 프로젝트 전반에서 새로운 이름으로 참조를 업데이트하고 테스트를 실행해 변경 영향이 없는지 검증하세요.

Comment on lines +3 to +7
public record Arrival (
Integer busType1, // 1이면 저상
Integer busType2, // 1이면 저상
Integer term // 배차 간격
) {}
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

OpenDataResponse.Item과 타입 정합성 확인 및 정렬

BusInfoService가 busType1/busType2를 문자열 "1"과 비교하는 흐름이 보입니다. Arrival은 Integer로 선언되어 있어 매핑/파싱 불일치 가능성이 있습니다. OpenDataResponse.Item의 필드 타입과 일치시키는 게 안전합니다. 불일치 시 런타임 변환/비교 버그가 날 수 있습니다.

아래 스크립트로 OpenDataResponse.Item의 선언 타입과 BusInfoService 비교 로직을 확인하세요. String이라면 Arrival도 String으로 맞추는 것을 권장합니다.

String으로 확인되면 다음과 같이 정렬을 권장합니다:

-public record Arrival (    
-    Integer busType1, // 1이면 저상
-    Integer busType2, // 1이면 저상
-    Integer term     // 배차 간격
-) {}
+public record Arrival (
+    String busType1, // "1"이면 저상
+    String busType2, // "1"이면 저상
+    String term      // 배차 간격(분), 문자열로 수신되므로 파싱 필요
+) {}

또는 Item이 Integer라면 BusInfoService의 비교 로직을 정수 비교로 정렬해 주세요.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "OpenDataResponse.Item 선언부 인근:"
rg -n -A 8 -B 2 $'record\\s+Item\\s*\\(' src/main/java/com/wayble/server/direction/external/opendata/dto/OpenDataResponse.java

echo
echo "BusInfoService에서 busType/term 사용부:"
rg -n -A 2 -B 2 'busType1|busType2|term\\(' src/main/java/com/wayble/server/direction/service/BusInfoService.java

Length of output: 831


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "BusInfoService에서 busType1/busType2 및 term 사용 내역:"
rg -n -C 4 'busType1|busType2|term' src/main/java/com/wayble/server/direction/service/BusInfoService.java

Length of output: 911


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Arrival 생성 위치 확인:"
rg -n -C 3 'new Arrival' -n src

Length of output: 98


Arrival 필드 타입을 OpenDataResponse.Item과 일치시키세요

OpenDataResponse.Item의 busType1, busType2, term이 모두 String으로 선언되어 있는데, Arrival은 Integer로 선언되어 있어 매핑/파싱 불일치 가능성이 있습니다. 런타임 변환/비교 버그를 방지하려면 두 레코드의 필드 타입을 일치시키십시오. 예를 들어, Arrival을 다음과 같이 수정하세요:

-public record Arrival(
-    Integer busType1, // 1이면 저상
-    Integer busType2, // 1이면 저상
-    Integer term      // 배차 간격
-) {}
+public record Arrival(
+    String busType1, // "1"이면 저상
+    String busType2, // "1"이면 저상
+    String term      // 배차 간격(분), 문자열로 수신되므로 파싱 필요
+) {}

만약 OpenDataResponse.Item이 Integer 타입으로 변경된다면, BusInfoService에서 "1".equals(item.busType1()) 비교와 Integer.parseInt(item.term()) 호출을 Integer 비교/처리로 모두 일치시켜 주세요.

📝 Committable suggestion

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

Suggested change
public record Arrival (
Integer busType1, // 1이면 저상
Integer busType2, // 1이면 저상
Integer term // 배차 간격
) {}
public record Arrival(
String busType1, // "1"이면 저상
String busType2, // "1"이면 저상
String term // 배차 간격(분), 문자열로 수신되므로 파싱 필요
) {}
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/direction/external/opendata/dto/Arrival.java
around lines 3 to 7, the Arrival record uses Integer for busType1, busType2, and
term while OpenDataResponse.Item declares these fields as String; change
Arrival's field types to String to match OpenDataResponse.Item so
mapping/parsing is consistent, and if OpenDataResponse.Item later changes to
Integer, update BusInfoService comparisons (replace
`"1".equals(item.busType1())` with Integer equality) and parses
(`Integer.parseInt(item.term())`) to use Integer values consistently.

Comment on lines +16 to +20
String stId,
String stNm,
String tmX,
String tmY
) {}
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

좌표 타입 및 좌표계 변환 확인 필요

tmX/tmY가 문자열로 선언되어 있습니다. TM 좌표를 사용하는 경우, 서비스 레이어에서 WGS84(lat/lon)로 변환하는 경로가 있는지 확인 부탁드립니다. 가능하다면 숫자형(double/BigDecimal) 매핑이 후처리에 유리합니다.

TM→WGS84 변환 로직 또는 사용 위치를 점검하려면 다음을 실행해 주세요:


🏁 Script executed:

#!/bin/bash
# tmX/tmY 사용 및 변환 여부 탐색
rg -n "tmX|tmY|StationSearchResponse" -A 5

Length of output: 8946


dto 및 서비스 레이어에서 TM 좌표 취급/변환 검토 필요

  • DTO 매핑

    • src/main/java/com/wayble/server/direction/external/opendata/dto/StationSearchResponse.java:16–20
    • tmX/tmY가 현재 String으로 선언되어 있어 파싱 시 Null/포맷 오류 발생 가능
    • 가급적 double 또는 BigDecimal로 매핑해 입력 단계에서 타입 안전성 확보를 권장
  • 좌표계 변환 로직

    • src/main/java/com/wayble/server/direction/service/BusInfoService.java:174–186 (findClosestStation 메서드)
    • 현재 String→double 파싱 후 TM 좌표계 상에서 거리만 계산하고, WGS84(lat/lon) 변환은 미구현
    • 이후 위경도 기반 로직(지도 렌더링, 클라이언트 전송 등)이 있다면 TM→WGS84 변환이 반드시 필요
    • Proj4j 또는 GeoTools 같은 라이브러리 도입을 검토하여 변환 로직을 추가할 것
🤖 Prompt for AI Agents
In
src/main/java/com/wayble/server/direction/external/opendata/dto/StationSearchResponse.java
around lines 16–20, tmX/tmY are declared as String which risks null/format
errors; change them to a numeric type (double or BigDecimal), ensure
Jackson/mapper will deserialize accordingly and add null/format validation. In
src/main/java/com/wayble/server/direction/service/BusInfoService.java around
lines 174–186 (findClosestStation), stop parsing Strings to double inline and
consume the numeric DTO values instead, then add TM→WGS84 conversion (using
Proj4j or GeoTools) before any distance calculations or before returning
coordinates to clients, update method signatures as needed, and add unit tests
to cover null/invalid inputs and coordinate conversion correctness.

Comment on lines +31 to +93
public TransportationResponseDto.BusInfo getBusInfo(String stationName, Long busId, Double x, Double y) {
List<Boolean> isLowFloor = new ArrayList<>();
Integer dispatchInterval = null;

boolean isShuttleBus = false;
if (busId != null) {
var route = routeRepository.findById(busId);
isShuttleBus = route.isPresent() && route.get().getRouteName().contains("마포");
}

try {
// 1. 정류소명으로 정류소 검색
StationSearchResponse stationSearchResponse = fetchStationByName(stationName);
if (stationSearchResponse == null || stationSearchResponse.msgBody() == null ||
stationSearchResponse.msgBody().itemList() == null ||
stationSearchResponse.msgBody().itemList().isEmpty()) {
log.warn("정류소를 찾을 수 없습니다: {}", stationName);
return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null);
}

// 2. 여러 정류소가 나올 때, 가장 가까운 정류소 찾기
StationSearchResponse.StationItem closestStation = findClosestStation(
stationSearchResponse.msgBody().itemList(), x, y);

if (closestStation == null) {
log.warn("가장 가까운 정류소를 찾을 수 없습니다: {}", stationName);
return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null);
}

// 3. 정류소 ID로 버스 도착 정보 조회
OpenDataResponse openDataResponse = fetchArrivals(Long.parseLong(closestStation.stId()), busId);
if (openDataResponse == null || openDataResponse.msgBody() == null ||
openDataResponse.msgBody().itemList() == null) {
log.warn("버스 도착 정보를 찾을 수 없습니다: {}", closestStation.stId());
return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null);
}

// 4. 버스 정보 추출
int count = 0;
for (OpenDataResponse.Item item : openDataResponse.msgBody().itemList()) {
if (count >= 1) break; // busId가 null일 때는 최대 1개 노선만

// busType1과 busType2 추가
isLowFloor.add("1".equals(item.busType1()));
isLowFloor.add("1".equals(item.busType2()));

// term을 정수로 변환
try {
dispatchInterval = Integer.parseInt(item.term());
} catch (NumberFormatException e) {
dispatchInterval = 0;
}

count++;
}

} catch (Exception e) {
log.error("버스 정보 조회 중 오류 발생: {}", e.getMessage());
return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null);
}

return new TransportationResponseDto.BusInfo(isShuttleBus, isLowFloor, dispatchInterval);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

getBusInfo 메서드가 너무 복잡합니다

이 메서드는 여러 책임을 가지고 있어 단일 책임 원칙(SRP)을 위반합니다. 셔틀 버스 확인, 정류소 검색, 도착 정보 조회 등을 별도 메서드로 분리하면 가독성과 테스트 용이성이 향상됩니다.

다음과 같이 리팩토링하는 것을 제안합니다:

 public TransportationResponseDto.BusInfo getBusInfo(String stationName, Long busId, Double x, Double y) {
-    List<Boolean> isLowFloor = new ArrayList<>();
-    Integer dispatchInterval = null;
-    
-    boolean isShuttleBus = false;
-    if (busId != null) {
-        var route = routeRepository.findById(busId);
-        isShuttleBus = route.isPresent() && route.get().getRouteName().contains("마포");
-    }
+    boolean isShuttleBus = checkIfShuttleBus(busId);
     
     try {
         // 1. 정류소명으로 정류소 검색
         StationSearchResponse stationSearchResponse = fetchStationByName(stationName);
         if (stationSearchResponse == null || stationSearchResponse.msgBody() == null || 
             stationSearchResponse.msgBody().itemList() == null || 
             stationSearchResponse.msgBody().itemList().isEmpty()) {
             log.warn("정류소를 찾을 수 없습니다: {}", stationName);
             return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null);
         }
         
         // 2. 여러 정류소가 나올 때, 가장 가까운 정류소 찾기
         StationSearchResponse.StationItem closestStation = findClosestStation(
                 stationSearchResponse.msgBody().itemList(), x, y);
         
         if (closestStation == null) {
             log.warn("가장 가까운 정류소를 찾을 수 없습니다: {}", stationName);
             return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null);
         }
         
         // 3. 정류소 ID로 버스 도착 정보 조회
         OpenDataResponse openDataResponse = fetchArrivals(Long.parseLong(closestStation.stId()), busId);
         if (openDataResponse == null || openDataResponse.msgBody() == null || 
             openDataResponse.msgBody().itemList() == null) {
             log.warn("버스 도착 정보를 찾을 수 없습니다: {}", closestStation.stId());
             return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null);
         }
         
-        // 4. 버스 정보 추출
-        int count = 0;
-        for (OpenDataResponse.Item item : openDataResponse.msgBody().itemList()) {
-            if (count >= 1) break; // busId가 null일 때는 최대 1개 노선만
-            
-            // busType1과 busType2 추가
-            isLowFloor.add("1".equals(item.busType1()));
-            isLowFloor.add("1".equals(item.busType2()));
-            
-            // term을 정수로 변환
-            try {
-                dispatchInterval = Integer.parseInt(item.term());
-            } catch (NumberFormatException e) {
-                dispatchInterval = 0;
-            }
-            
-            count++;
-        }
+        return extractBusInfo(openDataResponse, isShuttleBus);
         
     } catch (Exception e) {
         log.error("버스 정보 조회 중 오류 발생: {}", e.getMessage());
         return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null);
     }
-    
-    return new TransportationResponseDto.BusInfo(isShuttleBus, isLowFloor, dispatchInterval);
 }
+
+private boolean checkIfShuttleBus(Long busId) {
+    if (busId == null) return false;
+    return routeRepository.findById(busId)
+        .map(route -> route.getRouteName().contains("마포"))
+        .orElse(false);
+}
+
+private TransportationResponseDto.BusInfo extractBusInfo(OpenDataResponse response, boolean isShuttleBus) {
+    List<Boolean> isLowFloor = new ArrayList<>();
+    Integer dispatchInterval = null;
+    
+    if (response.msgBody() != null && response.msgBody().itemList() != null && 
+        !response.msgBody().itemList().isEmpty()) {
+        OpenDataResponse.Item item = response.msgBody().itemList().get(0);
+        
+        isLowFloor.add("1".equals(item.busType1()));
+        isLowFloor.add("1".equals(item.busType2()));
+        
+        try {
+            dispatchInterval = Integer.parseInt(item.term());
+        } catch (NumberFormatException e) {
+            dispatchInterval = 0;
+        }
+    }
+    
+    return new TransportationResponseDto.BusInfo(isShuttleBus, isLowFloor, dispatchInterval);
+}
📝 Committable suggestion

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

Suggested change
public TransportationResponseDto.BusInfo getBusInfo(String stationName, Long busId, Double x, Double y) {
List<Boolean> isLowFloor = new ArrayList<>();
Integer dispatchInterval = null;
boolean isShuttleBus = false;
if (busId != null) {
var route = routeRepository.findById(busId);
isShuttleBus = route.isPresent() && route.get().getRouteName().contains("마포");
}
try {
// 1. 정류소명으로 정류소 검색
StationSearchResponse stationSearchResponse = fetchStationByName(stationName);
if (stationSearchResponse == null || stationSearchResponse.msgBody() == null ||
stationSearchResponse.msgBody().itemList() == null ||
stationSearchResponse.msgBody().itemList().isEmpty()) {
log.warn("정류소를 찾을 수 없습니다: {}", stationName);
return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null);
}
// 2. 여러 정류소가 나올 때, 가장 가까운 정류소 찾기
StationSearchResponse.StationItem closestStation = findClosestStation(
stationSearchResponse.msgBody().itemList(), x, y);
if (closestStation == null) {
log.warn("가장 가까운 정류소를 찾을 수 없습니다: {}", stationName);
return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null);
}
// 3. 정류소 ID로 버스 도착 정보 조회
OpenDataResponse openDataResponse = fetchArrivals(Long.parseLong(closestStation.stId()), busId);
if (openDataResponse == null || openDataResponse.msgBody() == null ||
openDataResponse.msgBody().itemList() == null) {
log.warn("버스 도착 정보를 찾을 수 없습니다: {}", closestStation.stId());
return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null);
}
// 4. 버스 정보 추출
int count = 0;
for (OpenDataResponse.Item item : openDataResponse.msgBody().itemList()) {
if (count >= 1) break; // busId가 null일 때는 최대 1개 노선만
// busType1과 busType2 추가
isLowFloor.add("1".equals(item.busType1()));
isLowFloor.add("1".equals(item.busType2()));
// term을 정수로 변환
try {
dispatchInterval = Integer.parseInt(item.term());
} catch (NumberFormatException e) {
dispatchInterval = 0;
}
count++;
}
} catch (Exception e) {
log.error("버스 정보 조회 중 오류 발생: {}", e.getMessage());
return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null);
}
return new TransportationResponseDto.BusInfo(isShuttleBus, isLowFloor, dispatchInterval);
}
public TransportationResponseDto.BusInfo getBusInfo(String stationName, Long busId, Double x, Double y) {
boolean isShuttleBus = checkIfShuttleBus(busId);
try {
// 1. 정류소명으로 정류소 검색
StationSearchResponse stationSearchResponse = fetchStationByName(stationName);
if (stationSearchResponse == null
|| stationSearchResponse.msgBody() == null
|| stationSearchResponse.msgBody().itemList() == null
|| stationSearchResponse.msgBody().itemList().isEmpty()) {
log.warn("정류소를 찾을 수 없습니다: {}", stationName);
return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null);
}
// 2. 여러 정류소가 나올 때, 가장 가까운 정류소 찾기
StationSearchResponse.StationItem closestStation = findClosestStation(
stationSearchResponse.msgBody().itemList(), x, y);
if (closestStation == null) {
log.warn("가장 가까운 정류소를 찾을 수 없습니다: {}", stationName);
return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null);
}
// 3. 정류소 ID로 버스 도착 정보 조회
OpenDataResponse openDataResponse = fetchArrivals(
Long.parseLong(closestStation.stId()), busId);
if (openDataResponse == null
|| openDataResponse.msgBody() == null
|| openDataResponse.msgBody().itemList() == null) {
log.warn("버스 도착 정보를 찾을 수 없습니다: {}", closestStation.stId());
return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null);
}
// 4. 버스 정보 추출
return extractBusInfo(openDataResponse, isShuttleBus);
} catch (Exception e) {
log.error("버스 정보 조회 중 오류 발생: {}", e.getMessage());
return new TransportationResponseDto.BusInfo(isShuttleBus, new ArrayList<>(), null);
}
}
private boolean checkIfShuttleBus(Long busId) {
if (busId == null) {
return false;
}
return routeRepository.findById(busId)
.map(route -> route.getRouteName().contains("마포"))
.orElse(false);
}
private TransportationResponseDto.BusInfo extractBusInfo(
OpenDataResponse response, boolean isShuttleBus) {
List<Boolean> isLowFloor = new ArrayList<>();
Integer dispatchInterval = null;
if (response.msgBody() != null
&& response.msgBody().itemList() != null
&& !response.msgBody().itemList().isEmpty()) {
OpenDataResponse.Item item = response.msgBody().itemList().get(0);
isLowFloor.add("1".equals(item.busType1()));
isLowFloor.add("1".equals(item.busType2()));
try {
dispatchInterval = Integer.parseInt(item.term());
} catch (NumberFormatException e) {
dispatchInterval = 0;
}
}
return new TransportationResponseDto.BusInfo(
isShuttleBus, isLowFloor, dispatchInterval);
}
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/direction/service/BusInfoService.java around
lines 31 to 93, the getBusInfo method mixes multiple responsibilities (shuttle
check, station search, nearest-station selection, arrival fetch, and parsing
arrival data); extract each responsibility into small private methods — e.g.,
private boolean isShuttleBus(Long busId), private
StationSearchResponse.StationItem findClosestStationByName(String stationName,
Double x, Double y), private OpenDataResponse fetchArrivalsForStation(String
stId, Long busId), and private Pair<List<Boolean>, Integer>
extractBusInfoFromArrivals(OpenDataResponse) — move the routeRepository lookup,
null checks, logging and NumberFormatException handling into those helpers, keep
the original return semantics and logging/exception behavior, and have
getBusInfo orchestrate calls to these new methods to improve readability and
testability.

Comment on lines +146 to +147
"&stSrch=" + stationName +
"&resultType=json";
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

URL 인코딩이 필요합니다

정류소명에 특수문자나 공백이 포함될 경우 API 호출이 실패할 수 있습니다.

URL 인코딩을 추가하세요:

+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;

 String uri = openDataProperties.baseUrl() + 
         openDataProperties.endpoints().stationByName() +
         "?serviceKey=" + serviceKey +
-        "&stSrch=" + stationName +
+        "&stSrch=" + URLEncoder.encode(stationName, StandardCharsets.UTF_8) +
         "&resultType=json";
📝 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
"&stSrch=" + stationName +
"&resultType=json";
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
String uri = openDataProperties.baseUrl() +
openDataProperties.endpoints().stationByName() +
"?serviceKey=" + serviceKey +
"&stSrch=" + URLEncoder.encode(stationName, StandardCharsets.UTF_8) +
"&resultType=json";
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/direction/service/BusInfoService.java around
lines 146-147, the stationName is concatenated into the query string without
URL-encoding which can break calls when it contains spaces or special
characters; update the URL construction to URL-encode stationName (and any other
query parameter values) using java.net.URLEncoder with UTF-8 (e.g.,
URLEncoder.encode(stationName, StandardCharsets.UTF_8.name())) before
concatenation, handle or avoid the checked exception appropriately (use
StandardCharsets to prevent UnsupportedEncodingException or wrap in try/catch),
and use the encoded value in the final URL.

package com.wayble.server.direction.service;

import com.wayble.server.direction.dto.TransportationResponseDto;
import com.wayble.server.direction.dto.response.TransportationResponseDto;
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

외부 API 에러/Null 응답 방어 코드 및 로깅 레벨 보완 권장

현재 block() 이후 response/body/item null 케이스에 대한 방어가 없어 NPE 위험이 있습니다. 또한 오류 로깅이 info 레벨이며 e.getCause()만 출력해 원인 파악이 어렵습니다.

아래와 같이 보완을 권장합니다:

private Map<String, Boolean> getToiletInfo(Facility facility){
    String uri = UriComponentsBuilder.fromPath("/api/vulnerableUserInfo/stationDisabledToilet")
            .queryParam("serviceKey", kricProperties.key())
            .queryParam("format", "json")
            .queryParam("railOprLsttCd", facility.getRailOprLsttCd())
            .queryParam("lnCd", facility.getLnCd())
            .queryParam("stinCd", facility.getStinCd())
            .toUriString();

    List<KricToiletRawItem> items;
    try {
        KricToiletRawResponse response = kricWebClient
                .get()
                .uri(uri)
                .retrieve()
                // .onStatus(HttpStatusCode::isError, ... ) // 필요 시 상태별 처리
                .bodyToMono(KricToiletRawResponse.class)
                .block();

        if (response == null || response.body() == null || response.body().item() == null) {
            log.warn("KRIC 화장실 API 빈 응답: uri={}", uri);
            return new HashMap<>();
        }
        items = response.body().item();

    } catch (Exception e) {
        log.warn("KRIC 화장실 API 호출 실패: uri={}", uri, e);
        return new HashMap<>();
    }

    Map<String, Boolean> stationToiletMap = new HashMap<>();
    for (KricToiletRawItem item : items) {
        String stinCd = item.stinCd();
        int toiletCount = 0;
        try {
            toiletCount = Integer.parseInt(item.toltNum());
        } catch (NumberFormatException e) {
            log.warn("화장실 개수 파싱 실패 stinCd={}, raw={}", stinCd, item.toltNum(), e);
        }
        stationToiletMap.put(stinCd, stationToiletMap.getOrDefault(stinCd, false) || toiletCount > 0);
    }
    return stationToiletMap;
}
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/direction/service/FacilityService.java around
line 3, the WebClient .block() call result is used without null checks and
exceptions are logged at info with only e.getCause(), which risks NPEs and loses
error context; update the method to wrap the call in a try/catch that logs the
full exception at warn (include the exception object), verify response != null
&& response.body() != null && response.body().item() != null before using items
(return an empty map if any are null), parse numeric fields like toltNum() with
NumberFormatException handling (log parsing failures at warn with the raw
value), and ensure the method returns an empty Map on error or empty response
rather than allowing NPEs.

💡 Verification agent

🧩 Analysis chain

WebClient 빈 다중 정의로 인한 주입 모호성 위험 — @qualifier 지정 필요

WebClientConfig에 webClient/tMapWebClient/kricWebClient가 공존합니다. 현재 필드 타입이 WebClient 하나라 @qualifier가 없으면 NoUniqueBeanDefinitionException 위험이 큽니다.

아래와 같이 필드 또는 생성자 파라미터에 @qualifier("kricWebClient")를 지정해 주세요:

@RequiredArgsConstructor
public class FacilityService {
    private final FacilityRepository facilityRepository;
    private final @Qualifier("kricWebClient") WebClient kricWebClient;
    private final KricProperties kricProperties;
}

또는

public FacilityService(
    FacilityRepository facilityRepository,
    @Qualifier("kricWebClient") WebClient kricWebClient,
    KricProperties kricProperties
) { ... }

검증용 스크립트:


🏁 Script executed:

#!/bin/bash
# WebClient 빈 정의와 사용 위치 점검
rg -n "@Bean\\s*\\n\\s*public\\s+WebClient" -A 5
rg -n "WebClient\\s+[a-zA-Z0-9_]*WebClient"

Length of output: 731


명확한 WebClient 빈 주입을 위해 @qualifier 지정 필요

WebClientConfig에 tMapWebClient, kricWebClient 두 개의 WebClient 빈이 정의되어 있습니다. 현재 FacilityService에서 타입만으로 주입 중이므로 빈이 여러 개일 경우 NoUniqueBeanDefinitionException 위험이 있습니다. 빈 이름과 파라미터 이름 일치로 주입이 동작할 수 있으나, 빌드 설정이나 코드 변경 시 깨질 수 있어 명시적인 @qualifier 사용을 권장합니다.

  • 위치: src/main/java/com/wayble/server/direction/service/FacilityService.java:28
  • 수정 예시:
@RequiredArgsConstructor
 public class FacilityService {
     private final FacilityRepository facilityRepository;
-    private final WebClient kricWebClient;
+    private final @Qualifier("kricWebClient") WebClient kricWebClient;
     private final KricProperties kricProperties;
 }

또는 생성자 파라미터에 @qualifier 적용:

-public FacilityService(
-    FacilityRepository facilityRepository,
-    WebClient kricWebClient,
-    KricProperties kricProperties
-) { ... }
+public FacilityService(
+    FacilityRepository facilityRepository,
+    @Qualifier("kricWebClient") WebClient kricWebClient,
+    KricProperties kricProperties
+) { ... }
📝 Committable suggestion

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

Suggested change
import com.wayble.server.direction.dto.response.TransportationResponseDto;
@RequiredArgsConstructor
public class FacilityService {
private final FacilityRepository facilityRepository;
- private final WebClient kricWebClient;
+ private final @Qualifier("kricWebClient") WebClient kricWebClient;
private final KricProperties kricProperties;
}
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/direction/service/FacilityService.java around
line 28, the class injects WebClient by type while your WebClientConfig defines
multiple beans (tMapWebClient, kricWebClient), which can cause
NoUniqueBeanDefinitionException; update the injection to explicitly specify
which bean to use by adding @Qualifier("tMapWebClient") (or
@Qualifier("kricWebClient") as appropriate) on the WebClient field or on the
constructor parameter so Spring injects the intended named bean unambiguously.

Comment on lines +453 to +458
busInfo.isLowFloor() != null && !busInfo.isLowFloor().isEmpty() &&
busInfo.dispatchInterval() != null &&
busInfo.isLowFloor().stream().allMatch(floor -> !floor) &&
busInfo.dispatchInterval() == 0) {
return new ArrayList<>();
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

버스 정보 검증 로직 개선 필요

저상버스가 없고 배차간격이 0일 때 빈 리스트를 반환하는 것은 너무 엄격할 수 있습니다. 사용자에게 경로 옵션을 제공하되 경고를 표시하는 것이 더 나을 수 있습니다.

다음과 같이 수정을 고려하세요:

-if (busInfo != null && 
-    busInfo.isLowFloor() != null && !busInfo.isLowFloor().isEmpty() && 
-    busInfo.dispatchInterval() != null &&
-    busInfo.isLowFloor().stream().allMatch(floor -> !floor) &&
-    busInfo.dispatchInterval() == 0) {
-    return new ArrayList<>();
-}
+// 저상버스 미운행 및 배차간격 정보 없음을 로그로 기록하되, 경로는 반환
+if (busInfo != null && 
+    busInfo.isLowFloor() != null && !busInfo.isLowFloor().isEmpty() && 
+    busInfo.dispatchInterval() != null &&
+    busInfo.isLowFloor().stream().allMatch(floor -> !floor) &&
+    busInfo.dispatchInterval() == 0) {
+    log.warn("경로 {} - 저상버스 미운행, 배차간격 정보 없음", routeName);
+}
📝 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
busInfo.isLowFloor() != null && !busInfo.isLowFloor().isEmpty() &&
busInfo.dispatchInterval() != null &&
busInfo.isLowFloor().stream().allMatch(floor -> !floor) &&
busInfo.dispatchInterval() == 0) {
return new ArrayList<>();
}
// 저상버스 미운행 및 배차간격 정보 없음을 로그로 기록하되, 경로는 반환
if (busInfo != null &&
busInfo.isLowFloor() != null && !busInfo.isLowFloor().isEmpty() &&
busInfo.dispatchInterval() != null &&
busInfo.isLowFloor().stream().allMatch(floor -> !floor) &&
busInfo.dispatchInterval() == 0) {
log.warn("경로 {} - 저상버스 미운행, 배차간격 정보 없음", routeName);
}

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

Labels

release 프로덕션 서버에 배포

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FIX] 대중교통 길찾기의 환승 방식 조정 [FEAT] 길찾기 중 추가적인 정보 반영

2 participants