Skip to content

Conversation

@hyoinYang
Copy link
Contributor

@hyoinYang hyoinYang commented Aug 19, 2025

✔️ 연관 이슈

📝 작업 내용

진행상황을 main으로 머지합니다

스크린샷 (선택)

Summary by CodeRabbit

  • New Features

    • 시설별 엘리베이터 위치 조회 및 안내에 실제 위치 반영.
    • 사용자 장소 기능 확장: 리스트 생성 응답 확장, 다중 웨이블존 추가/삭제 API 및 관련 DTO 추가/교체.
    • 인증 개선: 인증 오류 enum 추가 및 JWT 기반 현재 사용자 추출 경로 추가.
  • Refactor

    • Wayble Zone 검색을 근접 후보(약 30m) + 텍스트 유사도(정규화·레벤슈타인·자카드)로 재구성.
    • 내부 타입 네임스페이스 정리(DirectionType 패키지 이동).
  • Tests

    • 통합 테스트 데이터 100→1000 확대 및 성능 측정/로깅 추가.
  • Chores

    • OpenAPI에 현재 사용자 파라미터 숨김 처리 및 일부 응답/검증 메타데이터 정리.

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

coderabbitai bot commented Aug 19, 2025

Walkthrough

DirectionType가 패키지로 이동되어 관련 import들이 갱신되었습니다. Elevator 엔티티는 latitude/longitude를 제거하고 location:String으로 대체되었고 ElevatorRepository와 FacilityService의 엘리베이터 연동이 추가되었습니다. WaybleZone 검색 로직이 근접 후보 + 메모리 유사도 방식으로 교체되고 인증 리졸버에 JWT/Authorization 처리 및 AuthErrorCase가 추가되었습니다.

Changes

Cohort / File(s) Summary
DirectionType 패키지 이동 및 참조 수정
src/main/java/com/wayble/server/direction/entity/type/DirectionType.java, .../direction/dto/response/TransportationResponseDto.java, .../direction/entity/transportation/Edge.java, .../direction/entity/transportation/Node.java, .../direction/entity/transportation/Route.java, .../direction/service/TransportationService.java
enum DirectionType 를 com.wayble.server.direction.entity.type로 이동하고 관련 import들을 업데이트.
Elevator 엔티티 및 저장소 추가
src/main/java/com/wayble/server/direction/entity/transportation/Elevator.java, src/main/java/com/wayble/server/direction/repository/ElevatorRepository.java
Elevator에서 latitude/longitude 제거, location:String 필드 추가. ElevatorRepository 추가 및 findByFacility(Facility) JPQL 메서드 구현.
FacilityService 엘리베이터 연동
src/main/java/com/wayble/server/direction/service/FacilityService.java
ElevatorRepository 주입 및 getElevatorInfo(Facility, Long) 추가하여 시설별 엘리베이터 위치 조회·정리 후 NodeInfo에 반영.
WaybleZone 검색 알고리즘 교체 및 유틸 추가
src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java
ES 다중매치 대신 30m geo 후보 조회 후 메모리 내 정규화·Levenshtein/Jaccard 유사도로 필터링하여 반환하도록 로직 변경 및 유틸 메서드 추가.
통합 테스트 조정
src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java
테스트 샘플 100→1000 증가, 거리 관련 검증 제거, 응답 시간 계측/로그 추가.
인증 리졸버 개선 및 에러 케이스 추가
src/main/java/com/wayble/server/auth/resolver/CurrentUserArgumentResolver.java, src/main/java/com/wayble/server/auth/exception/AuthErrorCase.java
JwtTokenProvider 주입 및 Authorization Bearer 처리 추가. 인증 실패 시 ApplicationException(AuthErrorCase.UNAUTHORIZED) 사용. AuthErrorCase enum 추가.
UserPlace API 및 DTO/서비스 변경
src/main/java/com/wayble/server/user/controller/UserPlaceController.java, src/main/java/com/wayble/server/user/service/UserPlaceService.java, src/main/java/com/wayble/server/user/dto/*.java, src/main/java/com/wayble/server/user/exception/UserErrorCase.java
장소 생성 API 요청/응답 DTO 변경(createPlaceList → UserPlaceCreateRequest/Response), 색상 기본 처리 및 중복명 검사 추가, addZoneToPlaces/removeZoneFromPlace 엔드포인트 및 서비스 메서드 추가. UserErrorCase에 PLACE_TITLE_DUPLICATED 추가.
OpenAPI 파라미터 숨김 적용
src/main/java/com/wayble/server/review/controller/ReviewController.java, src/main/java/com/wayble/server/user/controller/UserPlaceController.java
@Parameter(hidden = true)@CurrentUser 파라미터에 추가하여 Swagger에서 숨김 처리.
경미한 테스트 수정
src/test/.../ReviewServiceTest.java
Mockito verify 인자 변경(addReviewCount(1L) → addReviewCount(1)).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant FacilityService
  participant ElevatorRepo as ElevatorRepository
  participant DB

  Client->>FacilityService: getNodeInfo(facility, routeId)
  alt facility 코드 유효
    FacilityService->>ElevatorRepo: findByFacility(facility)
    ElevatorRepo->>DB: SELECT e FROM Elevator e WHERE e.facility = :facility
    DB-->>ElevatorRepo: List<Elevator>
    ElevatorRepo-->>FacilityService: elevators
    FacilityService->>FacilityService: extract/trim/sort locations
    FacilityService-->>Client: NodeInfo (with elevator locations)
  else 오류/없음
    FacilityService-->>Client: NodeInfo (elevator 빈 리스트)
  end
Loading
sequenceDiagram
  autonumber
  participant API
  participant Repo as WaybleZoneQuerySearchRepository
  participant ES

  API->>Repo: findSimilarWaybleZone(cond(zoneName, lat, lon))
  Repo->>ES: geoDistance query (30m, size=10)
  ES-->>Repo: nearby candidates
  Repo->>Repo: normalize & isTextSimilar(candidate.name, cond.zoneName)
  alt match found
    Repo-->>API: WaybleZoneSearchResponseDto.from(doc, null)
  else no match
    Repo-->>API: null
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Assessment against linked issues

Objective Addressed Explanation
기존 elevator 엔티티 수정 / 지하철 정보에 elevator 정보 추가 (#165)
내 장소 생성·추가·삭제 API 및 색상/카운트 처리 (#154)
내 장소 요약 조회·페이징·Eager 로딩 최적화 (#154) Controller/Service 쪽 페이징 및 엔드포인트 변경은 포함됨. Repository 수준의 Eager 로딩 최적화(특정 repo 메서드 변경)는 PR에서 명확하지 않음.
리뷰 작성에서 user_id 제거 및 인증으로 사용자 추출 (#154)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
WaybleZone 검색 로직 교체 (src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java) #154/#165 주된 목표(유저 장소·엘리베이터)는 아님; 검색 알고리즘 변경은 별도 기능 개선으로 보임.
통합 테스트의 거리 검증 제거 및 성능 계측 추가 (src/test/.../WaybleZoneSearchApiIntegrationTest.java) 테스트 변경은 linked issues의 기능 요구와 직접 관련이 없음(검증 기준 완화 및 성능 로깅 추가).

Possibly related PRs

Suggested reviewers

  • zyovn
  • KiSeungMin
  • wonjun-lee-fcwj245

Poem

"당근 들고 달려온 토끼예요 🐇
좌표 둘이 사라지고 한 줄로 쏙— location이라네
엘리베이터 위치 모아 정리해오고
숨은 주인 JWT로 찾아왔네 🛡️
깡충깡충 머지 축하, 당근 한 입!"

✨ 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.

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

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

690-706: SUBWAY 분기에서 NPE 가능성 (route null 체크 누락)

currentEdge.getRoute()가 null일 수 있는데 getRouteId()를 바로 호출합니다. 안전하게 null 체크가 필요합니다.

다음 패치를 적용해 주세요:

-            } else if (currentType == DirectionType.SUBWAY) {
+            } else if (currentType == DirectionType.SUBWAY) {
                 try {
-                    if (currentEdge.getStartNode() != null) {
-                        TransportationResponseDto.NodeInfo nodeInfo = facilityService.getNodeInfo(currentEdge.getStartNode().getId(), currentEdge.getRoute().getRouteId());
+                    if (currentEdge.getStartNode() != null && currentEdge.getRoute() != null) {
+                        TransportationResponseDto.NodeInfo nodeInfo = facilityService.getNodeInfo(
+                            currentEdge.getStartNode().getId(),
+                            currentEdge.getRoute().getRouteId()
+                        );
                         subwayInfo = new TransportationResponseDto.SubwayInfo(
                             nodeInfo.wheelchair(), 
                             nodeInfo.elevator(), 
                             nodeInfo.accessibleRestroom()
                         );
-                    } else {
+                    } else {
                         subwayInfo = new TransportationResponseDto.SubwayInfo(
                             new ArrayList<>(),
                             new ArrayList<>(),
                             false
                         );
                     }
                 } catch (Exception e) {
                     log.error("지하철 정보 조회 실패: {}", e.getMessage());
                     subwayInfo = new TransportationResponseDto.SubwayInfo(
                         new ArrayList<>(),
                         new ArrayList<>(),
                         false
                     );
                 }
             }

865-882: 환승 횟수 계산 로직 오류 — 동일 노선 연속 이용도 환승으로 계산됨

현재는 같은 모드·같은 노선명인 경우에도 환승으로 카운트됩니다. 동일 노선 연속 이용은 환승이 아니어야 합니다.

다음과 같이 조건을 정리해 주세요:

  • 모드가 다르면 환승 +1
  • 모드는 같고 노선명이 서로 다르면 환승 +1
  • 모드는 같고 노선명이 같으면 환승 +0
-                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++;
-                    }
-                }
+                if (previousMode != null) {
+                    if (previousMode == step.mode()) {
+                        if (previousRouteName != null && step.routeName() != null &&
+                            !previousRouteName.equals(step.routeName())) {
+                            transferCount++;
+                        }
+                    } else {
+                        transferCount++;
+                    }
+                }
src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java (1)

593-599: Null distance 포맷팅으로 인한 NPE 발생 가능

/validate 응답의 distance는 현재 null(리포지토리에서 distance 미계산)이므로, String.format("%.3f km", dto.distance())에서 NPE가 발생합니다. null 안전 처리 필요합니다.

적용 diff:

-        System.out.println("  거리: " + String.format("%.3f km", dto.distance()));
+        Double distance = dto.distance();
+        System.out.println("  거리: " + (distance != null ? String.format("%.3f km", distance) : "N/A"));
src/main/java/com/wayble/server/direction/service/FacilityService.java (1)

69-81: 엘리베이터 조회가 화장실 코드 유효성에 종속되어 누락될 수 있음 (논리 오류)

stinCd/railOprLsttCd/lnCd 중 하나라도 null이면 현재 엘리베이터 정보도 조회하지 않습니다. 엘리베이터는 내부 DB에서 facility 연관으로 조회 가능하므로, 화장실 코드 유무와 무관하게 조회해야 합니다. 아래처럼 엘리베이터 조회를 조건문 밖으로 이동해 주세요.

-                if (stinCd != null && railOprLsttCd != null && lnCd != null) {
-                    Map<String, Boolean> toiletInfo = getToiletInfo(facility);
-                    accessibleRestroom = toiletInfo.getOrDefault(stinCd, false);
-                    
-                    elevator = getElevatorInfo(facility, routeId);
-                } else {
+                if (stinCd != null && railOprLsttCd != null && lnCd != null) {
+                    Map<String, Boolean> toiletInfo = getToiletInfo(facility);
+                    accessibleRestroom = toiletInfo.getOrDefault(stinCd, false);
+                } else {
                     log.error("Facility 정보 누락 - nodeId: {}, stinCd: {}, railOprLsttCd: {}, lnCd: {}", 
                         nodeId, stinCd, railOprLsttCd, lnCd);
                 }
+                // 엘리베이터 정보는 시설 코드 유무와 무관하게 DB에서 조회
+                elevator = getElevatorInfo(facility, routeId);
🧹 Nitpick comments (17)
src/main/java/com/wayble/server/direction/entity/type/DirectionType.java (1)

6-8: 운송 수단 타입과 경로 마커(TO_/FROM_WAYPOINT) 혼재 — 분리 고려

DB에 EnumType.STRING으로 저장되는 엔티티 필드(Node.nodeType, Edge.edgeType)에서 실수로 마커 값이 영속될 가능성이 있습니다. 운송 수단(BUS/SUBWAY/WALK)과 경로 마커(TO_/FROM_WAYPOINT)를 별도 enum으로 분리하거나, 마커는 서비스 레이어 전용(비영속) 타입으로 관리하는 방안을 고려해주세요.

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

38-46: ID 수동 설정과 @GeneratedValue의 충돌 가능성

createEdge에서 ID를 외부에서 받아 빌더에 설정하지만, 엔티티는 @GeneratedValue(IDENTITY)를 사용합니다. 현재는 비영속 그래프용 임시 객체로 쓰는 듯하나, 실수로 영속화될 경우 식별자 충돌/예외 가능성이 있습니다. 임시 그래프 Edge는 별도의 DTO/레코드로 분리하거나, 엔티티를 재사용하더라도 ID는 null로 두는 형태(팩토리 메서드에서 ID 제거)를 권장합니다.

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

485-486: 불변 리스트에 add 가능성 — computeIfAbsent로 방어

graph.getOrDefault(currId, List.of())는 키가 없을 때 불변 리스트를 반환하므로 이후 currentEdges.add(...)에서 UnsupportedOperationException 위험이 있습니다. 안전하게 computeIfAbsent로 교체하세요.

-            List<Edge> currentEdges = graph.getOrDefault(curr.getId(), List.of());
+            List<Edge> currentEdges = graph.computeIfAbsent(curr.getId(), k -> new ArrayList<>());

129-133: OutOfMemoryError 캐치 지양

OutOfMemoryError는 복구 불가능한 Error 계열입니다. 잡아서 일반 예외처럼 숨기면 장애 원인 파악이 어려워집니다. 일반 예외만 캐치하고, OOME는 상위로 전파되도록 두는 편이 좋습니다.

-        } catch (OutOfMemoryError e) {
-            log.error("Out of memory error in transportation route finding: {}", e.getMessage());
-            throw new ApplicationException(PATH_NOT_FOUND);
+        } catch (Exception e) {
+            log.error("예상치 못한 예외로 경로 탐색에 실패했습니다: {}", e.getMessage());
+            throw new ApplicationException(PATH_NOT_FOUND);
         }

400-401: 임시 WALK 엣지의 중복 ID 사용

임시 도보 엣지에 고정 ID(-1, -2 등)를 반복 사용합니다. 현재는 비영속이라 문제 없지만, 실수로 영속되면 중복 키/예상치 못한 동작이 발생할 수 있습니다. ID는 null로 두거나(엔티티 재사용 시), 그래프 전용 DTO를 쓰는 방향을 권장합니다.

Also applies to: 413-414

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

21-26: 주석 보정 제안 (모드 예시)

현재 주석의 예시에 START/FINISH가 포함되어 있으나, 실제 Step.mode로는 사용되지 않습니다. 혼동 방지를 위해 예시를 정리해주세요.

-            DirectionType mode, // 예: START, WALK, SUBWAY, BUS, FINISH
+            DirectionType mode, // 예: WALK, SUBWAY, BUS
src/main/java/com/wayble/server/direction/entity/transportation/Route.java (1)

3-3: 동일 패키지에 대한 와일드카드 import 제거 권장

동일 패키지(com.wayble.server.direction.entity.transportation)는 import가 필요 없습니다. 불필요한 와일드카드 import를 제거하면 코드 가독성과 정적 분석 신뢰도가 개선됩니다.

적용 diff:

-import com.wayble.server.direction.entity.transportation.*;
src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java (5)

138-147: 30m 고정 반경 + 첫 매치 반환은 비결정적일 수 있음(정렬 고려 권장)

30m 내 후보만 가져와 텍스트 유사도 첫 매치를 반환하면, 동일 반경 내 복수 후보가 있을 때 결과가 비결정적으로 변할 수 있습니다. 근접도 기준 정렬(오름차순) 후 첫 매치를 선택하거나, 가장 높은 유사도(또는 거리+유사도 가중치)로 선택하는 방식을 고려해 주세요.


152-175: 근접 후보 조회 시 거리 정렬 추가 제안(결과 안정성 향상)

ES에서 30m 후보를 가져올 때 거리 정렬을 함께 사용하면 스트림의 첫 매치가 가장 가까운 후보가 되어 결과가 안정적입니다.

적용 diff:

         NativeQuery nativeQuery = NativeQuery.builder()
-                .withQuery(geoQuery)
+                .withQuery(geoQuery)
+                .withSort(SortOptions.of(s -> s
+                        .geoDistance(gds -> gds
+                                .field("address.location")
+                                .location(GeoLocation.of(gl -> gl
+                                        .latlon(ll -> ll
+                                                .lat(cond.latitude())
+                                                .lon(cond.longitude())
+                                        )
+                                ))
+                                .order(SortOrder.Asc)
+                        )
+                ))
                 .withPageable(PageRequest.of(0, 10)) // 30m 이내는 보통 10개 미만
                 .build();

180-206: 유사도 임계치 상수화 및 재사용 제안

0.7(레벤슈타인)과 0.6(자카드) 임계치는 의미 있는 매개변수입니다. 상수로 분리하면 추후 튜닝/AB 테스트가 용이합니다. 또한 검색어 정규화 결과를 외부에서 한번 계산하여 재사용하면(예: 스트림 이전) 후보가 증가해도 비용을 줄일 수 있습니다.


220-228: 레벤슈타인 유사도: 양쪽이 빈 문자열인 경우 1.0 반환 고려

현재는 한쪽이라도 빈 문자열이면 0.0을 반환합니다. 양쪽 모두 빈 문자열일 때는 동등으로 간주해 1.0을 반환하는 편이 일관적입니다.

적용 diff:

-    if (s1.isEmpty() || s2.isEmpty()) {
-        return 0.0;
-    }
+    if (s1.isEmpty() && s2.isEmpty()) {
+        return 1.0;
+    }
+    if (s1.isEmpty() || s2.isEmpty()) {
+        return 0.0;
+    }

259-277: 자카드 유사도: 문자 집합 기반의 한계 사항 안내

문자 “집합” 기반 자카드는 문자 중복과 순서를 반영하지 못합니다. 한글 환경에서 정확도를 높이려면 bigram/trigram(shingle) 기반 자카드 또는 토크나이저 기반(예: Nori) 접근을 중장기적으로 고려할 수 있습니다.

src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java (2)

83-83: 샘플 수 1000건은 CI 시간 증가 우려(태깅/프로파일 분리 권장)

성능 확인 목적이라면 @tag("perf") 또는 프로파일 분리로 일반 CI에서는 축소(예: 200)하고 성능 파이프라인에서만 대량 데이터를 사용하는 운용을 권장합니다.


539-556: 응답 시간 측정은 System.nanoTime 사용 권장(정밀도 향상)

HTTP 왕복 같은 상대 비교에는 nanoTime이 더 적합합니다. 큰 문제는 아니지만, 정밀도/일관성 향상을 위해 교체를 고려해 주세요.

적용 예:

long startTime = System.nanoTime();
// ...
long endTime = System.nanoTime();
long responseTimeMs = TimeUnit.NANOSECONDS.toMillis(endTime - startTime);
src/main/java/com/wayble/server/direction/repository/ElevatorRepository.java (1)

12-14: 파생 쿼리 메서드로 간결화 가능

현재 @query는 메서드 시그니처로 대체 가능합니다. 불필요한 @Query/@param 제거로 간결성/유지보수성 개선됩니다.

적용 diff:

-    @Query("SELECT e FROM Elevator e WHERE e.facility = :facility")
-    List<Elevator> findByFacility(@Param("facility") Facility facility);
+    List<Elevator> findByFacility(Facility facility);
src/main/java/com/wayble/server/direction/service/FacilityService.java (2)

73-73: getElevatorInfo의 routeId 파라미터가 사용되지 않습니다

혼란을 줄이기 위해 불필요한 파라미터를 제거하는 것이 좋습니다. 호출부도 함께 수정해 주세요.

-                    elevator = getElevatorInfo(facility, routeId);
+                    elevator = getElevatorInfo(facility);
-private List<String> getElevatorInfo(Facility facility, Long routeId) {
+private List<String> getElevatorInfo(Facility facility) {

Also applies to: 142-143


103-109: KRIC API 호출에 타임아웃이 없어 블로킹 리스크 존재

WebClient.block()에 타임아웃이 없어 외부 API 지연 시 무기한 대기할 수 있습니다. 운영 안전을 위해 합리적인 타임아웃을 추가하는 것을 권장합니다.

             KricToiletRawResponse response = kricWebClient
                     .get()
                     .uri(uri)
                     .retrieve()
-                    .bodyToMono(KricToiletRawResponse.class)
-                    .block();
+                    .bodyToMono(KricToiletRawResponse.class)
+                    .timeout(java.time.Duration.ofSeconds(3))
+                    .block();
📜 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 dc2c21c and bf997fc.

📒 Files selected for processing (11)
  • 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/Elevator.java (1 hunks)
  • src/main/java/com/wayble/server/direction/entity/transportation/Node.java (1 hunks)
  • src/main/java/com/wayble/server/direction/entity/transportation/Route.java (1 hunks)
  • src/main/java/com/wayble/server/direction/entity/type/DirectionType.java (1 hunks)
  • src/main/java/com/wayble/server/direction/repository/ElevatorRepository.java (1 hunks)
  • src/main/java/com/wayble/server/direction/service/FacilityService.java (4 hunks)
  • src/main/java/com/wayble/server/direction/service/TransportationService.java (1 hunks)
  • src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java (3 hunks)
  • src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java (4 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (9)
src/main/java/com/wayble/server/direction/entity/transportation/Edge.java (3)
src/main/java/com/wayble/server/direction/entity/DirectionType.java (1)
  • DirectionType (3-8)
src/main/java/com/wayble/server/direction/entity/Edge.java (1)
  • Edge (5-11)
src/main/java/com/wayble/server/direction/repository/EdgeRepository.java (1)
  • Repository (11-31)
src/main/java/com/wayble/server/direction/repository/ElevatorRepository.java (2)
src/main/java/com/wayble/server/direction/entity/transportation/Lift.java (1)
  • Entity (6-29)
src/main/java/com/wayble/server/direction/entity/transportation/Facility.java (1)
  • Entity (11-45)
src/main/java/com/wayble/server/direction/service/TransportationService.java (1)
src/main/java/com/wayble/server/direction/entity/DirectionType.java (1)
  • DirectionType (3-8)
src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java (1)
src/main/java/com/wayble/server/direction/entity/DirectionType.java (1)
  • DirectionType (3-8)
src/main/java/com/wayble/server/direction/entity/type/DirectionType.java (1)
src/main/java/com/wayble/server/direction/entity/DirectionType.java (1)
  • DirectionType (3-8)
src/main/java/com/wayble/server/direction/entity/transportation/Route.java (1)
src/main/java/com/wayble/server/direction/entity/DirectionType.java (1)
  • DirectionType (3-8)
src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java (1)
src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java (1)
  • Test (179-235)
src/main/java/com/wayble/server/direction/service/FacilityService.java (5)
src/main/java/com/wayble/server/direction/entity/transportation/Lift.java (1)
  • Entity (6-29)
src/main/java/com/wayble/server/direction/entity/transportation/Facility.java (1)
  • Entity (11-45)
src/main/java/com/wayble/server/direction/repository/FacilityRepository.java (1)
  • FacilityRepository (12-17)
src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java (1)
  • Test (179-235)
src/main/java/com/wayble/server/explore/entity/FacilityType.java (1)
  • FacilityType (3-6)
src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java (1)
src/main/java/com/wayble/server/explore/service/WaybleZoneSearchService.java (1)
  • Service (15-49)
🔇 Additional comments (10)
src/main/java/com/wayble/server/direction/entity/type/DirectionType.java (1)

1-1: 패키지 이동 적절합니다

com.wayble.server.direction.entity...entity.type로의 이동은 의미상 명확하고, 다른 클래스들의 import 갱신과도 일관됩니다.

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

11-11: import 경로 변경 OK

DirectionType의 패키지 이동에 맞춘 import 갱신이 정확합니다. 그 외 엔티티 매핑 변경은 없습니다.

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

3-3: import 경로 변경 OK

DirectionType의 새로운 패키지를 정확히 참조하고 있습니다.

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

10-10: import 경로 변경 OK

패키지 이동에 맞춘 DirectionType import 갱신이 정확합니다.

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

5-5: import 경로 변경 OK

DirectionType 패키지 이동에 맞춘 import 갱신이 정확합니다.

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

4-4: DirectionType import 경로 변경 LGTM

패키지 이동에 따른 import 수정이 정확합니다. 엔티티 필드 routeType@Enumerated(EnumType.STRING) 조합도 문제없습니다.

src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java (3)

22-22: Set import 추가 적절

자카드 유사도 계산용 Set 사용을 위한 import 추가는 적절합니다.


211-215: 정규화 로직 적절

공백/특수문자를 제거하고 소문자화하는 정규화는 한글/영문 혼재 환경에서 효과적입니다.


233-254: 표준 DP 레벤슈타인 구현 LGTM

시간복잡도 O(nm) 구현으로 후보 수(≤10)에서는 성능 이슈 우려 없습니다.

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

36-36: ElevatorRepository 주입 추가는 방향이 좋습니다

엘리베이터 정보를 지연 로딩 대신 전용 리포지토리로 조회하는 접근은 트랜잭션 경계 외 LazyInitializationException을 피하는 데 유효합니다.

Comment on lines +17 to +18
@Column(name = "location", nullable = false)
private String location;
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

위치 스키마 변경(DB 마이그레이션/레거시 사용처 점검 필요)

latitude/longitude → location 문자열 변경에 따라:

  • DB 마이그레이션(컬럼 추가/변경, 기존 lat/lon 제거 또는 비활성화)이 포함되었는지 확인 필요
  • 기존 코드에서 elevator.getLatitude()/getLongitude() 참조가 남아있지 않은지 점검 필요

확인을 위해 아래 스크립트로 레거시 사용처와 마이그레이션 흔적을 점검해 주세요. 결과 공유해 주시면 후속 지원하겠습니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for legacy Elevator latitude/longitude usages in Java code..."
rg -nP --type=java -C2 '\.getLatitude\s*\(|\.getLongitude\s*\(' || true
rg -nP --type=java -C2 '\bElevator\b.*(latitude|longitude)' || true
rg -nP --type=java -C2 '(latitude|longitude).*Elevator' || true

echo
echo "Searching for DB migration files mentioning elevator latitude/longitude..."
fd -a -t f -i '.*(migration|migrations|flyway|liquibase|changelog).*' || true
rg -nP -C2 '(?i)elevator.*(latitude|longitude|location)' || true

Length of output: 22107


DB 스키마 변경(엘리베이터 location 전환) 마이그레이션 및 레거시 호출 점검 필요

아래 사항을 반드시 반영/확인해주세요:

  • src/main/java/com/wayble/server/direction/entity/transportation/Elevator.java
    • latitude/longitudelocation 문자열 전환을 위한 Flyway/Liquibase 마이그레이션 스크립트 추가
      • 기존 lat/lon 컬럼에 저장된 값을 location으로 이관(migrate)
      • 값 이관 완료 후 lat/lon 컬럼 제거 또는 비활성화
  • 코드베이스 전역에서 elevator.getLatitude()elevator.getLongitude() 호출이 남아있지 않은지 재차 검증
    • 첫 검색 결과 해당 호출은 발견되지 않았으나, 커스텀 유틸이나 스크립트 등 예외 경로 점검 필요
  • 마이그레이션 스크립트는 src/main/resources/db/migration(또는 프로젝트 설정된 폴더)에 위치시키고, 자동 배포 파이프라인에 포함

위 작업 완료 후 결과(스크립트·검증 로그)를 공유해주시면 최종 승인 및 후속 지원을 진행하겠습니다.

🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/direction/entity/transportation/Elevator.java
around lines 17-18, the entity was changed from latitude/longitude fields to a
single location string but the DB migration and legacy-call cleanup are missing:
add a Flyway/Liquibase migration in src/main/resources/db/migration that (1)
adds the new location column if not present, (2) migrates existing
latitude/longitude values into the new location string format used by the
entity, and (3) drops or deprecates the old latitude/longitude columns after
verification; then search the entire codebase (including custom utils, scripts,
and tests) for any remaining elevator.getLatitude()/getLongitude() calls and
update them to use the new location accessor or parsing logic; ensure the
migration file is included in the automated deployment pipeline and provide
migration run logs and verification results.

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

🧹 Nitpick comments (1)
src/main/java/com/wayble/server/auth/resolver/CurrentUserArgumentResolver.java (1)

31-64: primitive long 지원 추가 및 숫자 검증 로직 분리 권장

현재 supportsParameter 메서드는 Long.class만 허용해 primitive long 파라미터를 받을 수 없습니다. 또한, resolveArgument 내에서 문자열 숫자 여부를 바로 검사하고 있어 가독성이 떨어집니다. 아래와 같이 개선을 제안합니다.

  • supportsParameter에 primitive long 추가 (파일: src/main/java/com/wayble/server/auth/resolver/CurrentUserArgumentResolver.java)
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        Class<?> type = parameter.getParameterType();
        return parameter.hasParameterAnnotation(CurrentUser.class)
    -           && Long.class.equals(type);
    +           && (Long.class.equals(type) || long.class.equals(type));
    }
  • 숫자 검증 로직을 헬퍼 메서드로 분리
    private boolean isNumeric(String s) {
        return s != null && s.chars().allMatch(Character::isDigit);
    }
    그리고 resolveArgument 내에서
    if (principal instanceof String s && isNumeric(s)) { … }
    …
    if (isNumeric(name)) { … }
  • 별도 등록 조치 불필요: WebMvcConfig.javaaddArgumentResolvers(...)로 이미 CurrentUserArgumentResolver가 등록되어 있습니다.
📜 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 bf997fc and cba4d8e.

📒 Files selected for processing (5)
  • src/main/java/com/wayble/server/auth/exception/AuthErrorCase.java (1 hunks)
  • src/main/java/com/wayble/server/auth/resolver/CurrentUserArgumentResolver.java (1 hunks)
  • src/main/java/com/wayble/server/review/controller/ReviewController.java (2 hunks)
  • src/main/java/com/wayble/server/user/controller/UserPlaceController.java (5 hunks)
  • src/test/java/com/wayble/server/review/service/ReviewServiceTest.java (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/main/java/com/wayble/server/user/controller/UserPlaceController.java (4)
src/main/java/com/wayble/server/user/service/UserPlaceService.java (1)
  • Service (27-137)
src/main/java/com/wayble/server/user/entity/UserPlaceWaybleZoneMapping.java (1)
  • Entity (10-29)
src/main/java/com/wayble/server/user/entity/UserPlace.java (1)
  • Entity (13-47)
src/main/java/com/wayble/server/user/controller/UserController.java (1)
  • PatchMapping (71-90)
src/test/java/com/wayble/server/review/service/ReviewServiceTest.java (2)
src/main/java/com/wayble/server/wayblezone/entity/WaybleZone.java (1)
  • addReviewCount (96-99)
src/main/java/com/wayble/server/review/service/ReviewService.java (1)
  • Service (23-81)
src/main/java/com/wayble/server/auth/exception/AuthErrorCase.java (10)
src/main/java/com/wayble/server/admin/exception/AdminErrorCase.java (1)
  • Getter (7-17)
src/main/java/com/wayble/server/user/exception/UserErrorCase.java (1)
  • Getter (7-30)
src/main/java/com/wayble/server/aws/AwsErrorCase.java (1)
  • Getter (7-17)
src/main/java/com/wayble/server/wayblezone/exception/WaybleZoneErrorCase.java (1)
  • Getter (7-18)
src/main/java/com/wayble/server/common/MainErrorCase.java (1)
  • Getter (7-16)
src/main/java/com/wayble/server/review/exception/ReviewErrorCase.java (1)
  • Getter (7-16)
src/main/java/com/wayble/server/common/exception/ErrorCase.java (1)
  • ErrorCase (3-10)
src/main/java/com/wayble/server/explore/exception/RecommendErrorCase.java (1)
  • Getter (7-20)
src/main/java/com/wayble/server/direction/exception/WalkingErrorCase.java (1)
  • Getter (7-21)
src/main/java/com/wayble/server/explore/exception/SearchErrorCase.java (1)
  • Getter (7-17)
src/main/java/com/wayble/server/auth/resolver/CurrentUserArgumentResolver.java (3)
src/main/java/com/wayble/server/common/config/security/jwt/JwtAuthenticationFilter.java (1)
  • RequiredArgsConstructor (13-43)
src/main/java/com/wayble/server/common/config/security/jwt/JwtAuthentication.java (1)
  • JwtAuthentication (7-19)
src/main/java/com/wayble/server/common/config/security/jwt/JwtTokenProvider.java (1)
  • getUserId (53-64)
⏰ 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 (5)
src/test/java/com/wayble/server/review/service/ReviewServiceTest.java (1)

69-69: Mockito verify 인수 타입 변경 적절

addReviewCount(1)로의 변경이 메서드 시그니처(long)와 호환되어 컴파일·런타임 모두 문제 없습니다. 테스트 의도도 명확합니다.

src/main/java/com/wayble/server/review/controller/ReviewController.java (1)

16-16: Swagger에서 @currentuser 파라미터 숨김 처리 👍

@Parameter(hidden = true) 적용으로 문서 노이즈를 줄이고, 클라이언트가 해당 값을 입력하려고 시도하는 실수를 방지합니다. 운영 관점에서도 일관성 있는 문서화 방식입니다.

Also applies to: 40-40

src/main/java/com/wayble/server/user/controller/UserPlaceController.java (2)

12-12: Swagger에서 @currentuser 파라미터 숨김 처리 일괄 적용 굿

4개 엔드포인트 모두 동일하게 숨겨 문서 일관성과 보안 의도를 잘 반영했습니다.

Also applies to: 38-38, 53-53, 70-70, 90-90


70-76: 페이지 인덱스 보정 구현 확인 완료
UserPlaceService.getZonesInPlace에서 요청된 1-based page

int zeroBased = Math.max(0, page - 1);
PageRequest.of(zeroBased, size, …)

로 올바르게 보정하고 있음을 확인했습니다. 별도 수정 불필요합니다.

src/main/java/com/wayble/server/auth/resolver/CurrentUserArgumentResolver.java (1)

31-64: SecurityContext + Bearer 토큰 폴백 처리로 견고성 향상

  • Long/Integer/숫자 문자열 principal 처리 + auth.getName() 숫자 처리로 범용성 확보
  • Authorization 헤더 Bearer 토큰 폴백 및 JWT 파싱 실패 시 ApplicationException(UNAUTHORIZED)로 일관된 오류 응답

테스트/운영 모두에서 인증 정보 부재 시 명확히 401을 반환할 수 있어 좋습니다.

[fix] 유저 웨이블존 저장 및 조회 관련 API 수정
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: 2

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/user/service/UserPlaceService.java (1)

115-118: 페이지 인덱스 오프바이원 버그 (0-based를 1-based로 잘못 보정).

요구사항에 따라 클라이언트는 page=0부터 전달합니다. 현재 Math.max(0, page - 1)는 page=1을 0페이지로 취급하여 2페이지 요청부터 모두 한 페이지씩 당겨집니다.

바로잡기:

-        int zeroBased = Math.max(0, page - 1);
-
-        Pageable pageable = PageRequest.of(zeroBased, size, Sort.by(Sort.Direction.DESC, "id"));
+        Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "id"));

추가로, 컨트롤러 레이어에서 page, size에 대해 @min(0), @min(1)/@max(...) 등의 제약을 걸어두면 입력 유효성도 보장할 수 있습니다.

🧹 Nitpick comments (11)
src/main/java/com/wayble/server/user/dto/UserPlaceCreateRequestDto.java (1)

5-8: color 길이 제약 추가 권장 (엔티티 스키마와 일치).

UserPlace.color 컬럼이 length=20으로 제한되어 있습니다. DTO에도 검증을 추가하여 조기 실패시키는 것이 안전합니다.

권장 변경사항:

 package com.wayble.server.user.dto;

 import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;

 public record UserPlaceCreateRequestDto(
         @NotBlank(message = "제목은 필수입니다.") String title,
-        String color
+        @Size(max = 20, message = "color는 최대 20자까지 가능합니다.") String color
 ) {}
src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java (1)

3-10: 컨테이너 요소 유효성 검사 및 과도한 배치 입력 방지 가드 추가 제안.

  • 현재 @notempty만으로는 리스트 내부 요소의 null을 막지 못합니다. 컨테이너 요소에 @NotNull을 적용해 주세요.
  • 매우 큰 입력으로 인한 부하를 방지하려면 리스트 크기에 상한을 두는 것도 고려해주세요(예: 최대 1000개).

권장 변경사항:

 import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
 import java.util.List;

 public record UserPlaceAddZonesRequestDto(
         @NotNull Long placeId,
-        @NotEmpty List<Long> waybleZoneIds
+        @NotEmpty @Size(max = 1000, message = "waybleZoneIds는 최대 1000개까지 허용됩니다.")
+        List<@NotNull Long> waybleZoneIds
 ) {}
src/main/java/com/wayble/server/user/exception/UserErrorCase.java (1)

25-26: 중복 리소스는 409 Conflict가 더 적절합니다 (일관성).

닉네임 중복(NICKNAME_DUPLICATED)이 409를 사용 중인데, PLACE_TITLE_DUPLICATED는 400입니다. 의미상 409가 적합하며, 코드베이스 내 일관성도 맞춰주세요.

권장 변경사항:

-    PLACE_TITLE_DUPLICATED(400, 1016, "동일한 이름의 리스트가 이미 있습니다.");
+    PLACE_TITLE_DUPLICATED(409, 1016, "동일한 이름의 리스트가 이미 있습니다.");
src/main/java/com/wayble/server/user/dto/UserPlaceCreateResponseDto.java (1)

5-11: UserPlaceCreateResponseDto의 message 필드 제거 및 빌더 호출 정리 권장

CommonResponse가 이미 응답 메시지를 제공하므로, DTO 내부의 중복된 message 필드를 제거하는 것을 권장합니다. 이 변경을 적용할 경우, 빌더 호출에서 해당 필드 관련 메서드도 함께 삭제해야 합니다.

  • src/main/java/com/wayble/server/user/dto/UserPlaceCreateResponseDto.java
    • record 정의에서 String message 필드 제거
  • src/main/java/com/wayble/server/user/controller/UserPlaceController.java
    • UserPlaceCreateResponseDto.builder() 체인에서 .message("리스트가 생성되었습니다.") 호출 삭제
src/main/java/com/wayble/server/user/service/UserPlaceService.java (4)

60-88: 대량 추가 성능 개선: N+1 쿼리/저장을 벌크로 치환.

현재 구현은

  • WaybleZone 개별 조회 N회
  • 매핑 존재 여부 exists 쿼리 N회
  • 매핑 save N회
  • WaybleZone save N회
    로 쿼리가 선형 증가합니다. 입력 리스트가 커질 경우 성능 저하가 큽니다.

권장 방향:

  • uniqueIds에 대해 WaybleZone을 한 번에 조회(findAllById).
  • 기존 매핑된 zoneId들을 IN 절로 조회해 Set으로 수집.
  • 신규 대상만 골라 매핑 saveAll, WaybleZone likes를 메모리에서 증가 후 saveAll.
  • place.savedCount는 신규 건수만큼 루프 혹은 addCount(n) 메서드로 누적.

예시 변경(리포지토리 보조 메서드 필요):

-    public void addZonesToPlace(Long userId, Long placeId, List<Long> waybleZoneIds) {
+    public void addZonesToPlace(Long userId, Long placeId, List<Long> waybleZoneIds) {
         UserPlace place = userPlaceRepository.findByIdAndUser_Id(placeId, userId)
                 .orElseThrow(() -> new ApplicationException(UserErrorCase.PLACE_NOT_FOUND));

         Set<Long> uniqueIds = new LinkedHashSet<>(waybleZoneIds);

-        int added = 0;
-        for (Long zoneId : uniqueIds) {
-            WaybleZone zone = waybleZoneRepository.findById(zoneId)
-                    .orElseThrow(() -> new ApplicationException(UserErrorCase.WAYBLE_ZONE_NOT_FOUND));
-
-            boolean exists = mappingRepository.existsByUserPlace_IdAndWaybleZone_Id(placeId, zoneId);
-            if (exists) continue;
-
-            mappingRepository.save(UserPlaceWaybleZoneMapping.builder()
-                    .userPlace(place)
-                    .waybleZone(zone)
-                    .build());
-
-            place.increaseCount();
-            zone.addLikes(1);
-            waybleZoneRepository.save(zone);
-
-            added++;
-        }
-
-        if (added > 0) userPlaceRepository.save(place);
+        // 1) 존재하는 Zone 일괄 조회 및 검증
+        List<WaybleZone> zones = waybleZoneRepository.findAllById(uniqueIds);
+        if (zones.size() != uniqueIds.size()) {
+            throw new ApplicationException(UserErrorCase.WAYBLE_ZONE_NOT_FOUND);
+        }
+        Map<Long, WaybleZone> zoneMap = zones.stream().collect(java.util.stream.Collectors.toMap(WaybleZone::getId, z -> z));
+
+        // 2) 이미 매핑된 zoneId 집합 조회
+        Set<Long> existing = mappingRepository.findWaybleZoneIdsByUserPlaceIdAndWaybleZoneIdIn(placeId, uniqueIds);
+
+        // 3) 신규 대상 선별
+        List<Long> toAdd = uniqueIds.stream().filter(id -> !existing.contains(id)).toList();
+        if (toAdd.isEmpty()) return;
+
+        // 4) 매핑 벌크 저장
+        List<UserPlaceWaybleZoneMapping> mappings = toAdd.stream()
+                .map(id -> UserPlaceWaybleZoneMapping.builder().userPlace(place).waybleZone(zoneMap.get(id)).build())
+                .toList();
+        mappingRepository.saveAll(mappings);
+
+        // 5) 카운트/좋아요 업데이트 벌크
+        toAdd.forEach(_id -> place.increaseCount());
+        List<WaybleZone> toUpdate = toAdd.stream().map(zoneMap::get).peek(z -> z.addLikes(1)).toList();
+        waybleZoneRepository.saveAll(toUpdate);
     }

리포지토리 보조 메서드 예시(별도 파일 수정 필요):

// UserPlaceWaybleZoneMappingRepository.java
@Query("select m.waybleZone.id from UserPlaceWaybleZoneMapping m where m.userPlace.id = :placeId and m.waybleZone.id in :zoneIds")
Set<Long> findWaybleZoneIdsByUserPlaceIdAndWaybleZoneIdIn(@Param("placeId") Long placeId, @Param("zoneIds") Collection<Long> zoneIds);

원하시면 관련 리포지토리/테스트 코드도 함께 제안드리겠습니다.


87-88: 명시적 save는 생략 가능 (영속성 컨텍스트 자동 flush).

place는 이미 영속 상태이므로, 변경 감지가 트랜잭션 커밋 시 반영됩니다. 불필요한 save 호출은 제거해도 됩니다.

권장 변경사항:

-        if (added > 0) userPlaceRepository.save(place);
+        // if (added > 0) userPlaceRepository.save(place); // 생략 가능

120-132: null 필드 직렬화 방지 또는 빈 컬렉션 사용 권장.

facilities(null)은 응답 JSON에 null로 표출될 수 있습니다(해당 DTO에 @JsonInclude 설정이 없다면). 빈 목록을 사용하는 편이 API 일관성에 유리합니다.

권장 변경사항:

-                        .facilities(null)
+                        .facilities(java.util.Collections.emptyList())

또는 WaybleZoneListResponseDto에 @JsonInclude(JsonInclude.Include.NON_NULL)를 추가해 null 필드가 출력되지 않도록 할 수 있습니다.


146-152: 삭제 경쟁 조건 완화 제안(선택).

exists → delete 순서에서 경쟁이 발생하면 존재 확인 직후 다른 트랜잭션이 삭제할 수 있습니다. deleteBy...가 삭제 행 수를 반환하도록 하고 0이면 PLACE_MAPPING_NOT_FOUND를 던지는 방식이 더 견고합니다.

가능하다면 리포지토리 메서드를 int deleteByUserPlace_IdAndWaybleZone_Id(...)로 정의하여 결과를 검사하세요.

src/main/java/com/wayble/server/user/controller/UserPlaceController.java (3)

6-6: 와일드카드 import(*) 대신 명시적 import로 가독성/안정성 개선 제안

빌드/IDE 설정에 따라 와일드카드가 예기치 않게 확장될 수 있어, 사용하는 DTO만 명시적으로 import하는 편이 안전합니다.

다음과 같이 변경을 제안합니다:

-import com.wayble.server.user.dto.*;
+import com.wayble.server.user.dto.UserPlaceAddZonesRequestDto;
+import com.wayble.server.user.dto.UserPlaceCreateRequestDto;
+import com.wayble.server.user.dto.UserPlaceCreateResponseDto;
+import com.wayble.server.user.dto.UserPlaceRemoveRequestDto;
+import com.wayble.server.user.dto.UserPlaceSummaryDto;

28-47: 생성 API의 응답 DTO 확장과 기본 색상 처리 좋습니다. 다만 403 응답 스펙 추가 및 기본값 처리 위치는 서비스/도메인으로 이동 권장

  • 설계: UserPlaceCreateResponseDto로 풍부한 응답을 제공하는 방향이 좋습니다.
  • 기본값: "GRAY" 기본값 처리는 서비스/도메인(엔티티 기본값 또는 팩토리)로 이동하면 컨트롤러 중복/지식 누수를 줄일 수 있습니다. 추후 다른 진입점에서도 동일 규칙을 자동 보장합니다.
  • 스웨거: 다른 엔드포인트들과의 일관성을 위해 403 응답 스펙을 추가하는 것이 좋습니다.

403 응답 추가에 대한 최소 diff:

     @ApiResponses({
-            @ApiResponse(responseCode = "200", description = "리스트 생성 성공"),
-            @ApiResponse(responseCode = "400", description = "동일한 리스트명이 이미 존재")
+            @ApiResponse(responseCode = "200", description = "리스트 생성 성공"),
+            @ApiResponse(responseCode = "400", description = "동일한 리스트명이 이미 존재"),
+            @ApiResponse(responseCode = "403", description = "권한이 없습니다.")
     })

82-95: 웨이블존 추가 API: 권한(403) 응답 스펙 추가 및 요청 유효성 강화(@notempty) 제안

  • 문서화: 다른 엔드포인트와 동일하게 403 응답을 추가하면 일관성이 좋아집니다.
  • 유효성: waybleZoneIds가 빈 배열일 경우 불필요한 호출을 막기 위해 @NotEmpty를 권장합니다(서버 자원 보호, 예측 가능한 오류 응답).

403 응답 추가 diff:

     @ApiResponses({
             @ApiResponse(responseCode = "200", description = "웨이블존 추가 성공"),
-            @ApiResponse(responseCode = "404", description = "유저/리스트/웨이블존을 찾을 수 없음")
+            @ApiResponse(responseCode = "404", description = "유저/리스트/웨이블존을 찾을 수 없음"),
+            @ApiResponse(responseCode = "403", description = "권한이 없습니다.")
     })

요청 DTO 유효성 예시(다른 파일에 적용):

// src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java
package com.wayble.server.user.dto;

import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.util.List;

public record UserPlaceAddZonesRequestDto(
    @NotNull Long placeId,
    @NotEmpty List<Long> waybleZoneIds
) {}

참고: 서비스 레벨에서 입력 리스트 중복 제거(Set 변환)도 권장합니다(동일 ID 중복 추가 방지).

📜 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 cba4d8e and 823d51b.

📒 Files selected for processing (7)
  • src/main/java/com/wayble/server/auth/exception/AuthErrorCase.java (1 hunks)
  • src/main/java/com/wayble/server/user/controller/UserPlaceController.java (4 hunks)
  • src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java (1 hunks)
  • src/main/java/com/wayble/server/user/dto/UserPlaceCreateRequestDto.java (1 hunks)
  • src/main/java/com/wayble/server/user/dto/UserPlaceCreateResponseDto.java (1 hunks)
  • src/main/java/com/wayble/server/user/exception/UserErrorCase.java (1 hunks)
  • src/main/java/com/wayble/server/user/service/UserPlaceService.java (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/wayble/server/auth/exception/AuthErrorCase.java
🧰 Additional context used
🧬 Code Graph Analysis (5)
src/main/java/com/wayble/server/user/dto/UserPlaceCreateResponseDto.java (6)
src/main/java/com/wayble/server/common/response/CommonResponse.java (1)
  • Builder (8-42)
src/main/java/com/wayble/server/user/dto/UserPlaceSummaryDto.java (1)
  • Builder (5-11)
src/main/java/com/wayble/server/user/dto/UserPlaceListResponseDto.java (2)
  • Builder (7-22)
  • Builder (13-21)
src/main/java/com/wayble/server/user/dto/UserPlaceRequestDto.java (1)
  • UserPlaceRequestDto (5-9)
src/main/java/com/wayble/server/user/dto/UserPlaceZonesResponseDto.java (1)
  • Builder (8-15)
src/main/java/com/wayble/server/user/entity/UserPlace.java (1)
  • Entity (13-47)
src/main/java/com/wayble/server/user/exception/UserErrorCase.java (2)
src/main/java/com/wayble/server/user/entity/UserPlaceWaybleZoneMapping.java (1)
  • Entity (10-29)
src/main/java/com/wayble/server/user/entity/UserPlace.java (1)
  • Entity (13-47)
src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java (4)
src/main/java/com/wayble/server/user/dto/UserPlaceRemoveRequestDto.java (1)
  • UserPlaceRemoveRequestDto (5-8)
src/main/java/com/wayble/server/user/dto/UserPlaceRequestDto.java (1)
  • UserPlaceRequestDto (5-9)
src/main/java/com/wayble/server/user/dto/UserPlaceZonesResponseDto.java (1)
  • Builder (8-15)
src/main/java/com/wayble/server/user/entity/UserPlaceWaybleZoneMapping.java (1)
  • Entity (10-29)
src/main/java/com/wayble/server/user/dto/UserPlaceCreateRequestDto.java (2)
src/main/java/com/wayble/server/user/dto/UserPlaceRequestDto.java (1)
  • UserPlaceRequestDto (5-9)
src/main/java/com/wayble/server/user/entity/UserPlace.java (1)
  • Entity (13-47)
src/main/java/com/wayble/server/user/controller/UserPlaceController.java (1)
src/main/java/com/wayble/server/user/dto/UserPlaceRemoveRequestDto.java (1)
  • UserPlaceRemoveRequestDto (5-8)
🔇 Additional comments (4)
src/main/java/com/wayble/server/user/dto/UserPlaceCreateRequestDto.java (1)

5-8: 검증 완료: @Valid 적용 확인됨

UserPlaceController#createPlaceList 메서드 시그니처에
@RequestBody @Valid UserPlaceCreateRequestDto request
가 이미 선언되어 있어, @NotBlank 기반 Bean Validation이 정상 동작합니다.

src/main/java/com/wayble/server/user/controller/UserPlaceController.java (3)

10-10: @parameter import 추가 적절합니다

하단의 @Parameter(hidden = true) 사용을 위한 import가 정확히 반영되었습니다.


57-57: @currentuser + @parameter(hidden = true) 적용 적절합니다

보안 컨텍스트로부터 userId를 주입하고 문서에서 숨기는 처리가 일관되게 적용되었습니다.


97-113: API 1-base 페이지 인덱스 처리 확인 — 변경 불필요

서비스 내부에서 Math.max(0, page - 1)로 1-base API를 0-base로 보정하여 PageRequest.of(zeroBased, size)를 사용하고 있으므로, 컨트롤러의 문서(page는 1부터 시작)와 기본값(defaultValue = "1")은 현재 로직과 일치합니다. 제안된 0-base 기본값 및 문구 변경은 반영할 필요가 없습니다.

Comment on lines 64 to 81
@DeleteMapping
@Operation(
summary = "장소에서 웨이블존 제거",
description = "RequestBody로 placeId, waybleZoneId를 받아 지정한 장소에서 웨이블존을 제거합니다."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "제거 성공"),
@ApiResponse(responseCode = "404", description = "장소 또는 매핑 정보를 찾을 수 없음"),
@ApiResponse(responseCode = "403", description = "권한이 없습니다.")
})
public CommonResponse<String> removeZoneFromPlace(
@Parameter(hidden = true) @CurrentUser Long userId,
@RequestBody @Valid UserPlaceRemoveRequestDto request
) {
userPlaceService.removeZoneFromPlace(userId, request.placeId(), request.waybleZoneId());
return CommonResponse.success("제거되었습니다.");
}

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

DELETE 본문(RequestBody) 사용은 호환성 이슈 소지가 큼 — 경로 변수 형식으로 변경 제안

여러 클라이언트/프록시/게이트웨이(일부 ALB, CDN, 레거시 프록시)에서 DELETE 본문을 미전달/무시하는 사례가 있습니다. 안전성을 위해 /{placeId}/zones/{waybleZoneId} 경로 변수로의 설계를 권장합니다. 또한 삭제는 200 바디 대신 204 No Content도 고려할 수 있습니다(프로젝트의 CommonResponse 규약에 맞추어 결정).

경로 변수로의 변경 예시:

-    @DeleteMapping
+    @DeleteMapping("/{placeId}/zones/{waybleZoneId}")
     @Operation(
             summary = "장소에서 웨이블존 제거",
-            description = "RequestBody로 placeId, waybleZoneId를 받아 지정한 장소에서 웨이블존을 제거합니다."
+            description = "경로 변수 placeId, waybleZoneId로 지정한 장소에서 웨이블존을 제거합니다."
     )
     @ApiResponses({
             @ApiResponse(responseCode = "200", description = "제거 성공"),
             @ApiResponse(responseCode = "404", description = "장소 또는 매핑 정보를 찾을 수 없음"),
             @ApiResponse(responseCode = "403", description = "권한이 없습니다.")
     })
-    public CommonResponse<String> removeZoneFromPlace(
-            @Parameter(hidden = true) @CurrentUser Long userId,
-            @RequestBody @Valid UserPlaceRemoveRequestDto request
-    ) {
-        userPlaceService.removeZoneFromPlace(userId, request.placeId(), request.waybleZoneId());
+    public CommonResponse<String> removeZoneFromPlace(
+            @Parameter(hidden = true) @CurrentUser Long userId,
+            @PathVariable Long placeId,
+            @PathVariable Long waybleZoneId
+    ) {
+        userPlaceService.removeZoneFromPlace(userId, placeId, waybleZoneId);
         return CommonResponse.success("제거되었습니다.");
     }

또한, 이 삭제 시 Issue #154의 요구사항인 savedCount 자동 감소가 서비스에서 수행되는지 확인 부탁드립니다. 아래 스크립트로 빠르게 점검할 수 있습니다.


🏁 Script executed:

#!/bin/bash
# UserPlaceService.removeZoneFromPlace 구현과 savedCount 처리 여부 점검

echo "== Locate removeZoneFromPlace implementation =="
rg -nP --type=java -C4 '\bclass\s+UserPlaceService\b|removeZoneFromPlace\s*\('

echo
echo "== savedCount 증가/감소 처리 흔적 검색 =="
rg -nP --type=java -C3 '\bsavedCount\b|\bincrementSavedCount\b|\bdecrementSavedCount\b|\bsetSavedCount\b'

Length of output: 7892


DELETE 본문 대신 경로 변수 사용 권고 및 savedCount 감소 로직 추가 필요

DELETE 요청 시 RequestBody가 일부 클라이언트/프록시에서 무시될 수 있으므로, 경로 변수 방식으로 변경을 권장합니다. 또한 removeZoneFromPlace 구현에서 매핑 삭제 후 savedCount를 감소시키는 로직이 누락된 것으로 확인되었습니다.

• UserPlaceController (src/main/java/com/wayble/server/user/controller/UserPlaceController.java)

  • 옵션:
    - @DeleteMapping
    + @DeleteMapping("/{placeId}/zones/{waybleZoneId}")
    ...
    - public CommonResponse<String> removeZoneFromPlace(
    -     @Parameter(hidden = true) @CurrentUser Long userId,
    -     @RequestBody @Valid UserPlaceRemoveRequestDto request
    - ) {
    -     userPlaceService.removeZoneFromPlace(userId, request.placeId(), request.waybleZoneId());
    + public CommonResponse<String> removeZoneFromPlace(
    +     @Parameter(hidden = true) @CurrentUser Long userId,
    +     @PathVariable Long placeId,
    +     @PathVariable Long waybleZoneId
    + ) {
    +     userPlaceService.removeZoneFromPlace(userId, placeId, waybleZoneId);

• UserPlaceService (src/main/java/com/wayble/server/user/service/UserPlaceService.java)

  • 필수: 매핑 삭제 직후 savedCount 감소 및 엔티티 저장 로직 추가
     public void removeZoneFromPlace(Long userId, Long placeId, Long waybleZoneId) {
         UserPlace place = userPlaceRepository.findByIdAndUser_Id(placeId, userId)
             .orElseThrow(() -> new ApplicationException(UserErrorCase.PLACE_NOT_FOUND));
         // ... 매핑 존재 여부 검증 및 매핑 조회
  •    mappingRepository.delete(mappingEntity);
    
  •    mappingRepository.delete(mappingEntity);
    
  •    // Issue #154: savedCount 감소 처리
    
  •    place.decreaseCount();
    
  •    userPlaceRepository.save(place);
    
    }
    
    

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

♻️ Duplicate comments (1)
src/main/java/com/wayble/server/user/controller/UserPlaceController.java (1)

106-122: DELETE의 RequestBody 사용은 호환성 이슈 소지 — 경로 변수로 변경 권장

여러 클라이언트/프록시에서 DELETE 본문이 미전달/무시될 수 있습니다. 경로 변수 사용으로 안전성을 높이세요. (이 코멘트는 이전 리뷰와 동일한 맥락입니다.)

권장 변경:

-    @DeleteMapping("/zones")
+    @DeleteMapping("/{placeId}/zones/{waybleZoneId}")
     @Operation(
-            summary = "내가 저장한 리스트에서 웨이블존 제거",
-            description = "RequestBody로 placeId, waybleZoneId를 받아 지정한 장소에서 웨이블존을 제거합니다."
+            summary = "내가 저장한 리스트에서 웨이블존 제거",
+            description = "경로 변수 placeId, waybleZoneId로 지정한 장소에서 웨이블존을 제거합니다."
     )
@@
-    public CommonResponse<String> removeZoneFromPlace(
-            @Parameter(hidden = true) @CurrentUser Long userId,
-            @RequestBody @Valid UserPlaceRemoveRequestDto request
-    ) {
-        userPlaceService.removeZoneFromPlace(userId, request.placeId(), request.waybleZoneId());
+    public CommonResponse<String> removeZoneFromPlace(
+            @Parameter(hidden = true) @CurrentUser Long userId,
+            @PathVariable Long placeId,
+            @PathVariable Long waybleZoneId
+    ) {
+        userPlaceService.removeZoneFromPlace(userId, placeId, waybleZoneId);
         return CommonResponse.success("성공적으로 제거되었습니다.");
     }

추가로, 204 No Content 사용 여부는 프로젝트의 CommonResponse 규약에 맞춰 검토 부탁드립니다.

🧹 Nitpick comments (5)
src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java (1)

7-10: ID 유효성 보강 제안: @positive 및 리스트 크기 제한 추가 권장

  • 현재 @notempty와 컨테이너 요소 @NotNull은 적절합니다. 다만 0/음수 ID를 차단하기 위해 @positive 추가, 과도한 대량 요청 방지를 위해 @SiZe(max=...) 제한을 권장합니다.
  • 서비스 레벨에서 중복 제거(LinkedHashSet)가 있지만, DTO 레벨에서도 기본적인 제약을 주면 방어력이 높아집니다.

적용 예시:

 import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
+import jakarta.validation.constraints.Size;
 import java.util.List;

 public record UserPlaceAddZonesRequestDto(
-        @NotEmpty List<@NotNull Long> placeIds,
-        @NotNull Long waybleZoneId
+        @NotEmpty @Size(max = 50) List<@NotNull @Positive Long> placeIds,
+        @NotNull @Positive Long waybleZoneId
 ) {}
  • max 값(예: 50)은 요구사항/운영 한도에 맞게 결정 바랍니다.
src/main/java/com/wayble/server/user/service/UserPlaceService.java (1)

83-85: save 호출 N회 → 배치 저장으로 최소화 권장

반복문 내 userPlaceRepository.save(place) 호출은 N회 DB 왕복을 유발합니다. 변경된 place들을 수집해 마지막에 saveAll(placeList) 호출로 줄이는 것을 고려해 주세요.

src/main/java/com/wayble/server/user/controller/UserPlaceController.java (3)

38-54: 정규화 로직의 중복은 서비스로 집약 권장 (응답 DTO를 서비스에서 생성)

컨트롤러에서 title/color 정규화를 다시 수행하면 서비스 로직과의 불일치/드리프트가 생깁니다. 서비스가 UserPlaceCreateResponseDto를 반환하게 하여 단일 진실 공급원으로 유지하세요.

권장 변경(컨트롤러):

-    public CommonResponse<UserPlaceCreateResponseDto> createPlaceList(
-            @Parameter(hidden = true) @CurrentUser Long userId,
-            @RequestBody @Valid UserPlaceCreateRequestDto request
-    ) {
-        Long placeId = userPlaceService.createPlaceList(userId, request);
-        String normalizedTitle = request.title().trim();
-        String normalizedColor = (request.color() == null || request.color().isBlank())
-                ? "GRAY"
-                : request.color().trim().toUpperCase();
-        return CommonResponse.success(
-                UserPlaceCreateResponseDto.builder()
-                        .placeId(placeId)
-                        .title(normalizedTitle)
-                        .color(normalizedColor)
-                        .message("리스트가 생성되었습니다.")
-                        .build()
-        );
-    }
+    public CommonResponse<UserPlaceCreateResponseDto> createPlaceList(
+            @Parameter(hidden = true) @CurrentUser Long userId,
+            @RequestBody @Valid UserPlaceCreateRequestDto request
+    ) {
+        return CommonResponse.success(userPlaceService.createPlaceList(userId, request));
+    }

권장 변경(서비스; 시그니처/반환 타입 변경 및 DTO 생성):

-    public Long createPlaceList(Long userId, UserPlaceCreateRequestDto request) {
+    public UserPlaceCreateResponseDto createPlaceList(Long userId, UserPlaceCreateRequestDto request) {
@@
-            return saved.getId();
+            return UserPlaceCreateResponseDto.builder()
+                    .placeId(saved.getId())
+                    .title(saved.getTitle())
+                    .color(saved.getColor())
+                    .message("리스트가 생성되었습니다.")
+                    .build();

73-86: 다중 리스트 추가 엔드포인트: 입력 한도/오류 요약 개선 제안

  • 대량 placeIds 요청 시 처리 한도(@SiZe(max=...))를 DTO에 명시하면 API 보호에 유익합니다.
  • 반환 메시지 외에 상세 결과(성공/스킵된 placeId 목록)를 포함한 응답 DTO를 제공하면 클라이언트 UX가 좋아집니다.

원하시면 응답 DTO 스펙과 컨트롤러/서비스 수정 패치를 제안하겠습니다.


96-104: placeId에도 @min(1) 추가 권장

page/size에는 제약이 있으나 placeId는 음수/0 허용 상태입니다. 간단히 @min(1)로 방어하는 것을 권장합니다.

적용 예시:

-    public CommonResponse<Page<WaybleZoneListResponseDto>> getZonesInPlace(
-            @Parameter(hidden = true) @CurrentUser Long userId,
-            @RequestParam Long placeId,
-            @RequestParam(defaultValue = "1") @Min(1) Integer page,
-            @RequestParam(defaultValue = "20") @Min(1) @Max(100) Integer size
-    ) {
+    public CommonResponse<Page<WaybleZoneListResponseDto>> getZonesInPlace(
+            @Parameter(hidden = true) @CurrentUser Long userId,
+            @RequestParam @Min(1) Long placeId,
+            @RequestParam(defaultValue = "1") @Min(1) Integer page,
+            @RequestParam(defaultValue = "20") @Min(1) @Max(100) Integer size
+    ) {
📜 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 823d51b and 3fe1ea9.

📒 Files selected for processing (6)
  • src/main/java/com/wayble/server/user/controller/UserPlaceController.java (2 hunks)
  • src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java (1 hunks)
  • src/main/java/com/wayble/server/user/dto/UserPlaceRequestDto.java (0 hunks)
  • src/main/java/com/wayble/server/user/dto/UserResponseDto.java (0 hunks)
  • src/main/java/com/wayble/server/user/exception/UserErrorCase.java (1 hunks)
  • src/main/java/com/wayble/server/user/service/UserPlaceService.java (4 hunks)
💤 Files with no reviewable changes (2)
  • src/main/java/com/wayble/server/user/dto/UserPlaceRequestDto.java
  • src/main/java/com/wayble/server/user/dto/UserResponseDto.java
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java (3)
src/main/java/com/wayble/server/user/dto/UserPlaceRemoveRequestDto.java (1)
  • UserPlaceRemoveRequestDto (5-8)
src/main/java/com/wayble/server/user/dto/UserPlaceRequestDto.java (1)
  • UserPlaceRequestDto (5-9)
src/main/java/com/wayble/server/user/entity/UserPlaceWaybleZoneMapping.java (1)
  • Entity (10-29)
src/main/java/com/wayble/server/user/exception/UserErrorCase.java (1)
src/main/java/com/wayble/server/user/entity/UserPlace.java (1)
  • Entity (13-47)
src/main/java/com/wayble/server/user/controller/UserPlaceController.java (2)
src/main/java/com/wayble/server/common/config/security/jwt/JwtAuthenticationFilter.java (1)
  • RequiredArgsConstructor (13-43)
src/main/java/com/wayble/server/user/dto/UserPlaceRemoveRequestDto.java (1)
  • UserPlaceRemoveRequestDto (5-8)
src/main/java/com/wayble/server/user/service/UserPlaceService.java (1)
src/main/java/com/wayble/server/user/entity/UserPlaceWaybleZoneMapping.java (1)
  • Entity (10-29)
⏰ 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 (5)
src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java (1)

8-8: 컨테이너 요소 제약 사용 적절

List<@NotNull Long>로 null 요소 방지한 점 좋습니다. Bean Validation 컨테이너 요소 제약이 활성화된 환경(예: Spring Boot 3.x + Hibernate Validator)에서 잘 동작합니다.

src/main/java/com/wayble/server/user/exception/UserErrorCase.java (1)

25-26: 409 Conflict로의 중복 타이틀 에러 추가 적절

  • PLACE_TITLE_DUPLICATED(409, 1016, …) 추가와 이전 항목 쉼표 처리 수정 모두 타당합니다.
  • 서비스에서 애플리케이션/DB 레벨 모두에서 이 에러로 일관 번역되는지 확인만 부탁드립니다.
src/main/java/com/wayble/server/user/service/UserPlaceService.java (3)

90-92: zone 저장을 1회로 모은 점 좋습니다

루프 후 added>0 일괄 저장으로 write 횟수를 최소화한 점 좋습니다.


120-120: 0-based 페이지 변환 로직 적합 (컨트롤러의 @min(1)과 연동 OK)

page - 1로 0 기반 변환이 맞습니다. 컨트롤러에서 page에 @min(1)을 적용했으므로 음수 위험도 없습니다.


39-58: DB 유니크 제약 확인 및 DataIntegrityViolation 예외 처리 추가 권장

  • 애플리케이션 레벨 선행 조회만으로는 동시 요청 시 중복 타이틀 생성이 가능합니다.
    → DB 레벨(user_place(user_id, title) + 활성 조건) 유니크 제약을 추가하고, DataIntegrityViolationException을 잡아 PLACE_TITLE_DUPLICATED로 매핑해 주세요.
  • request.title()이 null일 경우 NPE가 발생할 수 있습니다.
    → DTO에 @NotBlank 또는 비슷한 검증 어노테이션이 적용되어 있는지 확인 바랍니다.
  • DB 마이그레이션(예: src/main/resources/db/migration/)에서 user_place(user_id, title) 유니크 제약이 존재하는지 확인하고, 없으면 스크립트에 추가해 주세요.

수정 예시 (src/main/java/com/wayble/server/user/service/UserPlaceService.java):

     try {
         UserPlace saved = userPlaceRepository.save(
             UserPlace.builder()
                 .title(normalizedTitle)
                 .color(color)
                 .user(user)
                 .build()
         );
         return saved.getId();
-    } catch (Exception e) {
-        // ...
+    } catch (DataIntegrityViolationException e) {
+        // DB 유니크 제약 위반 → 애플리케이션 예외로 매핑
+        throw new ApplicationException(UserErrorCase.PLACE_TITLE_DUPLICATED);
     }

위 사항들이 모두 적용되었는지 확인 부탁드립니다.

Comment on lines +60 to 94
@Transactional
public int addZoneToPlaces(Long userId, List<Long> placeIds, Long waybleZoneId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND));

WaybleZone zone = waybleZoneRepository.findById(waybleZoneId)
.orElseThrow(() -> new ApplicationException(UserErrorCase.WAYBLE_ZONE_NOT_FOUND));

// 중복 저장 확인
boolean duplicated = mappingRepository.existsByUserPlace_User_IdAndWaybleZone_Id(userId, request.waybleZoneId());
if (duplicated) {
throw new ApplicationException(UserErrorCase.PLACE_ALREADY_SAVED);
Set<Long> uniquePlaceIds = new LinkedHashSet<>(placeIds);

int added = 0;
for (Long placeId : uniquePlaceIds) {
UserPlace place = userPlaceRepository.findByIdAndUser_Id(placeId, user.getId())
.orElseThrow(() -> new ApplicationException(UserErrorCase.PLACE_NOT_FOUND));

boolean exists = mappingRepository.existsByUserPlace_IdAndWaybleZone_Id(placeId, waybleZoneId);
if (exists) continue;

mappingRepository.save(UserPlaceWaybleZoneMapping.builder()
.userPlace(place)
.waybleZone(zone)
.build());

place.increaseCount();
userPlaceRepository.save(place);

zone.addLikes(1); // 리스트 하나에 추가될 때마다 +1
added++;
}

String color = (request.color() == null || request.color().isBlank()) ? "GRAY" : request.color();
UserPlace userPlace = userPlaceRepository.findByUser_IdAndTitle(userId, request.title())
.orElseGet(() -> userPlaceRepository.save(
UserPlace.builder()
.title(request.title())
.color(color)
.user(user)
.build()
));

mappingRepository.save(UserPlaceWaybleZoneMapping.builder()
.userPlace(userPlace)
.waybleZone(waybleZone)
.build());

userPlace.increaseCount();
userPlaceRepository.save(userPlace);

waybleZone.addLikes(1);
waybleZoneRepository.save(waybleZone);
if (added > 0) {
waybleZoneRepository.save(zone);
}
return added;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

다중 리스트 추가 시 경쟁 조건/성능 개선

  • 경쟁 조건: existsBy... → save 사이 레이스로 중복 매핑이 생길 수 있습니다. 매핑 테이블에 유니크 인덱스(user_place_id, wayble_zone_id)를 두고 save 시 DataIntegrityViolationException을 무시/변환하는 방식으로 보강하세요.
  • 성능: place 별 exists 쿼리/개별 save가 다수 발생합니다. 한 번에 “이미 매핑된 placeId 집합”을 조회하여 차집합만 insert하면 왕복 횟수를 줄일 수 있습니다.

적용 최소 변경(예시: 유니크 제약을 가정한 try/catch):

@@
-            boolean exists = mappingRepository.existsByUserPlace_IdAndWaybleZone_Id(placeId, waybleZoneId);
-            if (exists) continue;
-
-            mappingRepository.save(UserPlaceWaybleZoneMapping.builder()
-                    .userPlace(place)
-                    .waybleZone(zone)
-                    .build());
-
-            place.increaseCount();
-            userPlaceRepository.save(place);
-
-            zone.addLikes(1); // 리스트 하나에 추가될 때마다 +1
-            added++;
+            boolean exists = mappingRepository.existsByUserPlace_IdAndWaybleZone_Id(placeId, waybleZoneId);
+            if (exists) continue;
+            try {
+                mappingRepository.save(UserPlaceWaybleZoneMapping.builder()
+                        .userPlace(place)
+                        .waybleZone(zone)
+                        .build());
+                place.increaseCount();
+                userPlaceRepository.save(place);
+                zone.addLikes(1); // 리스트 하나에 추가될 때마다 +1
+                added++;
+            } catch (org.springframework.dao.DataIntegrityViolationException ex) {
+                // 동시성으로 인해 유니크 제약 위반 시 스킵
+            }

추가 최적화(선택): Repository에 다음과 같은 메서드를 추가해 이미 매핑된 placeId들을 한 번에 조회 후 차집합만 insert 하세요.

  • existsBy... 반복 대신: findAllPlaceIdsByWaybleZoneIdAndUserPlaceIdIn(waybleZoneId, placeIds)
  • 차집합 = 요청 placeIds - 이미 매핑된 placeIds
📝 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
@Transactional
public int addZoneToPlaces(Long userId, List<Long> placeIds, Long waybleZoneId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND));
WaybleZone zone = waybleZoneRepository.findById(waybleZoneId)
.orElseThrow(() -> new ApplicationException(UserErrorCase.WAYBLE_ZONE_NOT_FOUND));
// 중복 저장 확인
boolean duplicated = mappingRepository.existsByUserPlace_User_IdAndWaybleZone_Id(userId, request.waybleZoneId());
if (duplicated) {
throw new ApplicationException(UserErrorCase.PLACE_ALREADY_SAVED);
Set<Long> uniquePlaceIds = new LinkedHashSet<>(placeIds);
int added = 0;
for (Long placeId : uniquePlaceIds) {
UserPlace place = userPlaceRepository.findByIdAndUser_Id(placeId, user.getId())
.orElseThrow(() -> new ApplicationException(UserErrorCase.PLACE_NOT_FOUND));
boolean exists = mappingRepository.existsByUserPlace_IdAndWaybleZone_Id(placeId, waybleZoneId);
if (exists) continue;
mappingRepository.save(UserPlaceWaybleZoneMapping.builder()
.userPlace(place)
.waybleZone(zone)
.build());
place.increaseCount();
userPlaceRepository.save(place);
zone.addLikes(1); // 리스트 하나에 추가될 때마다 +1
added++;
}
String color = (request.color() == null || request.color().isBlank()) ? "GRAY" : request.color();
UserPlace userPlace = userPlaceRepository.findByUser_IdAndTitle(userId, request.title())
.orElseGet(() -> userPlaceRepository.save(
UserPlace.builder()
.title(request.title())
.color(color)
.user(user)
.build()
));
mappingRepository.save(UserPlaceWaybleZoneMapping.builder()
.userPlace(userPlace)
.waybleZone(waybleZone)
.build());
userPlace.increaseCount();
userPlaceRepository.save(userPlace);
waybleZone.addLikes(1);
waybleZoneRepository.save(waybleZone);
if (added > 0) {
waybleZoneRepository.save(zone);
}
return added;
}
@Transactional
public int addZoneToPlaces(Long userId, List<Long> placeIds, Long waybleZoneId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND));
WaybleZone zone = waybleZoneRepository.findById(waybleZoneId)
.orElseThrow(() -> new ApplicationException(UserErrorCase.WAYBLE_ZONE_NOT_FOUND));
Set<Long> uniquePlaceIds = new LinkedHashSet<>(placeIds);
int added = 0;
for (Long placeId : uniquePlaceIds) {
UserPlace place = userPlaceRepository.findByIdAndUser_Id(placeId, user.getId())
.orElseThrow(() -> new ApplicationException(UserErrorCase.PLACE_NOT_FOUND));
boolean exists = mappingRepository.existsByUserPlace_IdAndWaybleZone_Id(placeId, waybleZoneId);
if (exists) continue;
try {
mappingRepository.save(UserPlaceWaybleZoneMapping.builder()
.userPlace(place)
.waybleZone(zone)
.build());
place.increaseCount();
userPlaceRepository.save(place);
zone.addLikes(1); // 리스트 하나에 추가될 때마다 +1
added++;
} catch (org.springframework.dao.DataIntegrityViolationException ex) {
// 동시성으로 인해 유니크 제약 위반 시 스킵
}
}
if (added > 0) {
waybleZoneRepository.save(zone);
}
return added;
}
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/user/service/UserPlaceService.java around
lines 60-94, the current per-place exists()/save() loop causes a race condition
and extra DB round-trips; add a unique DB constraint on (user_place_id,
wayble_zone_id) and change the logic to (1) query existing mappings in bulk
using a new repository method like
findAllPlaceIdsByWaybleZoneIdAndUserPlaceIdIn(waybleZoneId, placeIds), (2)
compute the difference = requestedPlaceIds - alreadyMappedPlaceIds, (3) for only
the difference create UserPlaceWaybleZoneMapping entities and batch-save them,
increment the corresponding UserPlace counts and the WaybleZone likes in-memory,
then save updated entities once; additionally, keep a defensive try/catch around
the batch save to catch DataIntegrityViolationException and ignore/translate
duplicates (to handle concurrent inserts) so duplicates don’t fail the whole
transaction.

@seung-in-Yoo seung-in-Yoo merged commit 6c33eee into main Aug 20, 2025
3 checks passed
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.

[FEAT] 지하철의 승강기 정보 출력

4 participants