Skip to content

Feat/mypage favorite lockers#33

Open
buddle031 wants to merge 7 commits into
mainfrom
feat/mypage-favorite-lockers
Open

Feat/mypage favorite lockers#33
buddle031 wants to merge 7 commits into
mainfrom
feat/mypage-favorite-lockers

Conversation

@buddle031
Copy link
Copy Markdown
Collaborator

@buddle031 buddle031 commented May 14, 2026

✨ 작업 내용 한 줄 요약

  • 즐겨찾기 기능을 User_ID + Locker_ID 매핑 기반으로 구현하고 목록 조회 시 거리/최근 완료 제보 시각/POI 명칭 정책을 함께 반영했습니다.

🛠️ 작업 내용

  • 즐겨찾기 대상은 현재 기획 기준에서 “개별 보관함”으로 해석해 UserLockerFavorite 매핑 테이블로 설계했습니다. 한 사용자가 여러 보관함을 저장할 수 있고, 하나의 보관함도 여러 사용자에게 저장될 수 있으므로 User - Favorite - Locker 구조로 풀었습니다.
  • 즐겨찾기 목록은 저장된 즐겨찾기 자체의 스냅샷을 반환하지 않고 매핑된 Locker의 최신 정보를 다시 조합해 내려주도록 구현했습니다. 이 방식으로 보관함명/주소 등 표시 정보가 변경되더라도 목록에서 최신 값을 반영할 수 있게 했습니다.
  • 즐겨찾기 목록의 정렬은 단순 생성일 역순이 아니라 사용자 커스터마이징이 가능한 구조를 고려해 displayOrder 컬럼으로 별도 저장했습니다. 프론트는 최종 순서의 lockerIds 전체 배열을 전달하고, 서버는 해당 순서를 영속화합니다.
  • 즐겨찾기 목록의 거리 값은 현재 사용자 위치 기준으로 계산하고, 위치 정보가 없는 경우에도 목록이 비지 않도록 기본 좌표를 설정값으로 분리해 fallback 하도록 처리했습니다. 정책값을 코드에 하드코딩하지 않고 application.yaml 설정으로 분리해 이후 변경 비용을 낮췄습니다.
  • 목록의 “최근 업데이트 시간”은 단순 Locker 수정 시각이 아니라, 기획 의도에 맞춰 보관함별 최근 완료 제보 시각을 집계해서 내려주도록 구현했습니다. 이를 위해 locker_reports에서 보관함별 최신 완료 시각을 조회하는 쿼리를 추가했습니다.
  • 삭제된 보관함은 즐겨찾기 매핑이 남아 있더라도 사용자 목록/상태 조회에서는 자동 제외되도록 조회 조건을 조정했습니다. 데이터 보존과 사용자 노출 정책을 분리해 처리했습니다.
  • 초기 구현은 name 중심 응답이었지만, POI 기준으로 대표 명칭을 노출하기로 한 정책에 맞춰 목록 응답의 대표 필드를 poiName으로 정리했습니다. 현재는 LockerEntity.name을 POI 명칭으로 사용하고 있으며, 이후 building/floor 구조가 구체화되면 응답 모델을 추가 및 수정할 수 있습니다.
  • 즐겨찾기 버튼의 빠른 연속 클릭 상황을 고려해 중복 저장 시 unique constraint 충돌이 발생하더라도 최종 상태를 기준으로 처리되도록 저장 로직을 보완했습니다.

📚 참고 자료 (선택)

  • PM 확인사항 반영
    • 거리: 사용자 현재 위치 기준, 위치 미동의 시 기본 위치 기준
    • 최근 업데이트 시간: 최근 완료 제보 시각 기준
    • 대표 표시값: POI 명칭 기준

👀 리뷰 포인트 (선택)

  • 현재 즐겨찾기 대상은 Locker 기준으로 구현했습니다. 이후 building/floor 구조가 본격 도입되면 저장 구조보다는 목록 응답 모델(poiName, floor, 상세 이동 기준) 쪽이 먼저 바뀔 가능성이 있습니다.
  • “최근 업데이트 시간”은 별도 vote 엔티티가 아닌, 현재는 완료된 제보 이력의 최신 시각을 사용합니다.

Summary by CodeRabbit

  • 새로운 기능

    • 즐겨찾기 추가/제거/순서 변경 API 제공(순서 검증 포함)
    • 내 즐겨찾기 목록 페이지 조회(위치 기반 거리 표시, 마지막 완료 제보 시각 포함)
    • 특정 보관함의 즐겨찾기 상태 조회
  • 개선 사항

    • 삭제 처리된 보관함을 조회에서 제외하도록 필터링 강화
    • 기본 위치 설정 지원(위치 미제공 시 기본 좌표 사용)
    • 부재/비활성 보관함 조회 시 적절한 오류 코드 추가
  • 테스트

    • 즐겨찾기 API 및 리포지토리 동작 검증 테스트 추가

Review Change Stack

@buddle031 buddle031 requested a review from mike7643 as a code owner May 14, 2026 13:49
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

Warning

Rate limit exceeded

@buddle031 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 28 minutes and 11 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ec050104-6f8d-4fc9-b304-de096d16b3a3

📥 Commits

Reviewing files that changed from the base of the PR and between 4e194b6 and 4d8fc14.

📒 Files selected for processing (4)
  • src/main/java/com/zimdugo/locker/infrastructure/FavoriteLockerReaderAdapter.java
  • src/main/java/com/zimdugo/locker/infrastructure/LockerDistanceProjection.java
  • src/main/java/com/zimdugo/locker/infrastructure/LockerRepository.java
  • src/test/java/com/zimdugo/locker/infrastructure/FavoriteLockerReaderAdapterTest.java
📝 Walkthrough

Walkthrough

사용자 즐겨찾기 보관함의 도메인·영속성·인프라·애플리케이션 서비스·REST API·테스트 및 설정이 추가되어 조회, 상태 확인, 추가, 삭제, 순서 재정렬 기능이 구현되었습니다.

Changes

즐겨찾기 보관함 기능

Layer / File(s) Summary
도메인 계약 및 값 객체
src/main/java/com/zimdugo/locker/domain/FavoriteLocker.java, src/main/java/com/zimdugo/locker/domain/FavoriteLockerPage.java, src/main/java/com/zimdugo/locker/domain/FavoriteLockerReader.java, src/main/java/com/zimdugo/locker/domain/FavoriteLockerStore.java
FavoriteLocker 값 객체와 FavoriteLockerPage 페이징 모델, FavoriteLockerReader 읽기 인터페이스(findByUserId, existsByUserIdAndLockerId), FavoriteLockerStore 쓰기 인터페이스(add, remove, reorder)를 정의합니다.
데이터 영속성
src/main/java/com/zimdugo/locker/infrastructure/persistence/UserLockerFavoriteEntity.java, src/main/java/com/zimdugo/locker/infrastructure/persistence/LockerEntity.java, src/main/java/com/zimdugo/locker/infrastructure/LockerRepository.java, src/main/java/com/zimdugo/locker/infrastructure/LockerReportRepository.java, src/main/java/com/zimdugo/locker/infrastructure/LockerReportLatestUpdateProjection.java, src/main/java/com/zimdugo/locker/infrastructure/UserLockerFavoriteRepository.java, src/main/java/com/zimdugo/locker/infrastructure/LockerStoreAdapter.java, src/main/java/com/zimdugo/locker/infrastructure/LockerReportStoreAdapter.java
JPA 엔티티(UserLockerFavoriteEntity)와 복합 유니크·인덱스, LockerEntity의 소프트 삭제(deleted, markDeleted()), 리포지토리의 페이징·최신 완료 투표 시각 조회 메서드, 삭제된 보관함을 제외하는 쿼리 변경을 추가합니다.
인프라 어댑터 및 설정
src/main/java/com/zimdugo/locker/infrastructure/FavoriteLockerReaderAdapter.java, src/main/java/com/zimdugo/locker/infrastructure/FavoriteLockerStoreAdapter.java, src/main/java/com/zimdugo/locker/infrastructure/FavoriteLockerProperties.java
FavoriteLockerReaderAdapter는 페이징 조회, 원점 좌표 결정(요청 또는 설정), 하버사인 거리 계산, 락커별 최신 완료 투표 시각 일괄 조회 및 DTO 매핑을 구현합니다. FavoriteLockerStoreAdapter는 추가·삭제·재정렬(검증 및 일괄 업데이트) 로직과 동시성(데이터 무결성) 처리, FavoriteLockerProperties는 기본 원점 설정을 바인딩합니다.
애플리케이션 서비스
src/main/java/com/zimdugo/locker/application/FavoriteLockerItemResponse.java, src/main/java/com/zimdugo/locker/application/FavoriteLockerResponse.java, src/main/java/com/zimdugo/locker/application/FavoriteLockerStatusResponse.java, src/main/java/com/zimdugo/locker/application/FavoriteLockerQueryService.java, src/main/java/com/zimdugo/locker/application/FavoriteLockerCommandService.java
응답 DTO 레코드들과 FavoriteLockerQueryService(읽기 전용 트랜잭션: 목록·상태 조회 및 DTO 매핑), FavoriteLockerCommandService(add/remove/reorder를 저장소에 위임)을 추가합니다.
REST API
src/main/java/com/zimdugo/locker/entrypoint/FavoriteLockerOrderUpdateRequest.java, src/main/java/com/zimdugo/locker/entrypoint/LockerFavoriteApi.java, src/main/java/com/zimdugo/locker/entrypoint/LockerFavoriteController.java
요청 DTO에 @NotEmpty 및 원소 수준 검증(@NotNull, @Positive)을 적용하고, LockerFavoriteApi로 5개 엔드포인트 계약을 정의했으며 LockerFavoriteController에서 인증 추출, 서비스 호출 및 RestResponse 래핑을 구현합니다.
설정 및 테스트
src/main/resources/application.yaml, src/main/java/com/zimdugo/common/openapi/config/OpenApiConfig.java, src/main/java/com/zimdugo/core/exception/ErrorCode.java, src/test/java/com/zimdugo/locker/entrypoint/LockerFavoriteControllerTest.java, src/test/java/com/zimdugo/locker/infrastructure/FavoriteLockerReaderAdapterTest.java, src/test/java/com/zimdugo/locker/infrastructure/LockerReportRepositoryTest.java, src/test/java/com/zimdugo/locker/infrastructure/UserLockerFavoriteRepositoryTest.java
기본 원점 위경도 설정을 추가하고 OpenAPI 서버 항목을 단일 엔트리로 축소했으며 ErrorCode.LOCKER_NOT_FOUND를 추가했습니다. 컨트롤러·어댑터·리포지토리 단위·통합 테스트들이 추가 및 수정되었습니다.

Sequence Diagram

sequenceDiagram
  participant User
  participant Controller as LockerFavoriteController
  participant QueryService as FavoriteLockerQueryService
  participant ReaderAdapter as FavoriteLockerReaderAdapter
  participant StoreAdapter as FavoriteLockerStoreAdapter
  participant Database as Database

  User->>Controller: GET /api/v1/me/favorite-lockers?page=0&size=10&lat=37.5&lng=127.0
  Controller->>QueryService: getFavorites(userId, page, size, lat, lng)
  QueryService->>ReaderAdapter: findByUserId(userId, page, size, lat, lng)
  ReaderAdapter->>Database: UserLockerFavoriteRepository.findActiveFavoritesByUserId(...)
  Database-->>ReaderAdapter: Page<UserLockerFavoriteEntity>
  ReaderAdapter->>Database: LockerReportRepository.findLatestCompletedVoteAtByLockerIdIn(...)
  Database-->>ReaderAdapter: List<LockerReportLatestUpdateProjection>
  ReaderAdapter->>ReaderAdapter: calculateDistanceMeters(...) & map to FavoriteLocker
  ReaderAdapter-->>QueryService: FavoriteLockerPage
  QueryService-->>Controller: FavoriteLockerResponse
  Controller-->>User: 200 OK

  User->>Controller: PATCH /api/v1/me/favorite-lockers/order {lockerIds}
  Controller->>StoreAdapter: reorder(userId, lockerIds)
  StoreAdapter->>Database: UserLockerFavoriteRepository.findActiveFavoritesByUserIdAndLockerIds(...)
  Database-->>StoreAdapter: List<UserLockerFavoriteEntity>
  StoreAdapter->>Database: update displayOrder per requested order
  Controller-->>User: 200 OK
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested Reviewers

  • mike7643

Poem

🐰 즐겨찾기 추가하니 토끼가 깡충,
하버사인으로 거리도 재보고 팡팡,
순서 맞추니 정렬이 척척,
테스트로 확인해 안심하고 깔깔,
보관함 관리, 토끼도 기쁘네!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 즐겨찾기 기능 구현의 핵심을 간결하게 표현하며, 변경 사항의 주요 목적(마이페이지 즐겨찾기)을 명확히 전달합니다.
Description check ✅ Passed PR 설명이 필수 템플릿 섹션(한 줄 요약, 작업 내용, 참고 자료, 리뷰 포인트)을 완벽하게 따르며 매우 상세한 구현 내용과 설계 의도를 기술하고 있습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mypage-favorite-lockers

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

❤️ Share

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

Copy link
Copy Markdown

@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: 6

🧹 Nitpick comments (4)
src/main/java/com/zimdugo/locker/infrastructure/persistence/LockerEntity.java (1)

35-48: ⚡ Quick win

전역 소프트 삭제 필터 적용 고려

현재 deleted 필드를 추가했지만, 각 쿼리에서 수동으로 deleted = false 조건을 추가해야 합니다. Hibernate의 @SQLRestriction (또는 @Where in older versions)을 사용하면 엔티티 레벨에서 자동으로 필터링되어 실수로 삭제된 데이터를 조회하는 것을 방지할 수 있습니다.

♻️ 전역 필터 적용 제안
 `@Entity`
 `@Table`(name = "lockers")
+@SQLRestriction("deleted = false")
 `@Getter`
 `@NoArgsConstructor`(access = AccessLevel.PROTECTED)
 public class LockerEntity {

이렇게 하면 모든 조회 쿼리에서 자동으로 deleted = false 조건이 적용됩니다. 삭제된 레코드를 명시적으로 조회해야 하는 경우에는 네이티브 쿼리나 Session.enableFilter()를 사용할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/zimdugo/locker/infrastructure/persistence/LockerEntity.java`
around lines 35 - 48, Add a global soft-delete filter to LockerEntity by
annotating the entity class (LockerEntity) with Hibernate's `@Where`(clause =
"deleted = false") so all queries automatically exclude soft-deleted rows; keep
the existing deleted field and markDeleted() method but remove ad-hoc "deleted =
false" checks from repositories; also consider adding `@SQLDelete` and `@Loader` or
a repository method using Session.enableFilter()/native query when you must
explicitly load deleted records.
src/main/java/com/zimdugo/locker/infrastructure/persistence/UserLockerFavoriteEntity.java (1)

65-68: 💤 Low value

타임존 일관성을 위해 UTC 기준 시각 사용 고려

LocalDateTime.now()는 JVM의 시스템 타임존을 사용합니다. 분산 환경에서 서버 간 시각 불일치가 발생할 수 있으므로, Instant.now()를 사용하거나 데이터베이스의 CURRENT_TIMESTAMP를 활용하는 것이 더 안전합니다.

♻️ UTC 기준 시각 사용 제안

필드 타입을 Instant로 변경:

-    `@Column`(nullable = false)
-    private LocalDateTime createdAt;
+    `@Column`(nullable = false)
+    private Instant createdAt;

@PrePersist 훅 수정:

 `@PrePersist`
 protected void onCreate() {
-    this.createdAt = LocalDateTime.now();
+    this.createdAt = Instant.now();
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/zimdugo/locker/infrastructure/persistence/UserLockerFavoriteEntity.java`
around lines 65 - 68, The onCreate `@PrePersist` in UserLockerFavoriteEntity sets
createdAt with LocalDateTime.now(), which uses the JVM timezone; change the
createdAt field to an Instant (or offset-aware type) and set it using
Instant.now() in the onCreate method (or switch to database-side
CURRENT_TIMESTAMP by marking the column default), ensuring consistent UTC
timestamps; update any getters/setters/DTO mappings that reference createdAt to
handle Instant accordingly.
src/main/java/com/zimdugo/locker/infrastructure/FavoriteLockerProperties.java (1)

10-14: ⚡ Quick win

좌표 값 범위 검증 추가 권장

latitudelongitude 필드에 유효 범위 검증이 없습니다. 잘못된 설정 값은 거리 계산 시 런타임 오류나 부정확한 결과를 초래할 수 있습니다.

✅ 검증 애너테이션 추가 제안
+import jakarta.validation.constraints.DecimalMax;
+import jakarta.validation.constraints.DecimalMin;
+
 `@ConfigurationProperties`(prefix = "locker.favorite")
 public record FavoriteLockerProperties(
     DefaultOrigin defaultOrigin
 ) {
 
     public record DefaultOrigin(
+        `@DecimalMin`(value = "-90.0", message = "위도는 -90 이상이어야 합니다")
+        `@DecimalMax`(value = "90.0", message = "위도는 90 이하여야 합니다")
         double latitude,
+        `@DecimalMin`(value = "-180.0", message = "경도는 -180 이상이어야 합니다")
+        `@DecimalMax`(value = "180.0", message = "경도는 180 이하여야 합니다")
         double longitude
     ) {
     }
 }

@ConfigurationPropertiesBinding과 함께 사용하면 애플리케이션 시작 시 설정 값을 검증할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/zimdugo/locker/infrastructure/FavoriteLockerProperties.java`
around lines 10 - 14, Add range validation to the DefaultOrigin record by
annotating its latitude and longitude components with appropriate Bean
Validation constraints (latitude between -90 and 90, longitude between -180 and
180) and ensure the enclosing configuration properties class is validated at
startup (e.g., annotate the configuration properties class with `@Validated` or
use `@ConfigurationPropertiesBinding` as appropriate) so invalid settings fail
fast; update DefaultOrigin (the record) to use `@DecimalMin/`@DecimalMax or
equivalent annotations on the latitude and longitude parameters and enable
validation on the containing properties bean.
src/main/java/com/zimdugo/locker/infrastructure/FavoriteLockerStoreAdapter.java (1)

50-78: ⚡ Quick win

예외 타입을 코드베이스 규약(BusinessException + ErrorCode)에 맞춰 통일하는 것을 권장합니다.

같은 클래스 내 add()는 도메인 오류에 대해 BusinessException을 던지는 반면, reorder()IllegalArgumentException을 직접 던지고 있어 예외 처리/응답 매핑 계층에서 일관성 있게 다루기 어렵습니다. 클라이언트 입력 검증 실패에 해당하므로 적절한 ErrorCode를 정의해 BusinessException으로 일원화하는 편이 글로벌 예외 핸들러와 HTTP 응답 코드 매핑에 유리합니다.

♻️ 제안 예시
-        if (favoriteCount != lockerIds.size()) {
-            throw new IllegalArgumentException("Favorite locker order request must include all favorites.");
-        }
+        if (favoriteCount != lockerIds.size()) {
+            throw new BusinessException(ErrorCode.INVALID_FAVORITE_ORDER_REQUEST);
+        }
...
-        if (favorites.size() != lockerIds.size()) {
-            throw new IllegalArgumentException("Favorite locker order request contains unknown locker ids.");
-        }
+        if (favorites.size() != lockerIds.size()) {
+            throw new BusinessException(ErrorCode.INVALID_FAVORITE_ORDER_REQUEST);
+        }
...
-            if (favorite == null) {
-                throw new IllegalArgumentException("Favorite locker order request contains duplicate locker ids.");
-            }
+            if (favorite == null) {
+                throw new BusinessException(ErrorCode.INVALID_FAVORITE_ORDER_REQUEST);
+            }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/zimdugo/locker/infrastructure/FavoriteLockerStoreAdapter.java`
around lines 50 - 78, The reorder method in FavoriteLockerStoreAdapter currently
throws IllegalArgumentException for various validation failures; change these to
throw BusinessException with a specific ErrorCode (e.g., INVALID_FAVORITE_ORDER
or a new code that fits your error catalog) so it matches the add() behavior;
replace each IllegalArgumentException in reorder(...) with new
BusinessException(ErrorCode.YOUR_CHOICE, "descriptive message") (for
duplicate/unknown/count-mismatch cases), and ensure any callers/tests and the
global exception mapping expect that ErrorCode.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/com/zimdugo/locker/entrypoint/FavoriteLockerOrderUpdateRequest.java`:
- Around line 6-9: FavoriteLockerOrderUpdateRequest currently uses `@NotEmpty` on
lockerIds but does not validate individual elements; update the record
declaration to validate each list element by annotating the generic type: change
the parameter to List<@NotNull `@Positive` Long> lockerIds and add the imports
jakarta.validation.constraints.NotNull and
jakarta.validation.constraints.Positive so each element is checked for non-null
and positivity while retaining the existing `@NotEmpty` on the list itself.

In `@src/main/java/com/zimdugo/locker/entrypoint/LockerFavoriteApi.java`:
- Around line 118-124: The removeFavoriteLocker endpoint in LockerFavoriteApi
accepts a path variable lockerId without validation allowing zero or negative
values; add a Bean Validation annotation such as `@Positive` or `@Min`(1) to the
`@PathVariable` parameter (lockerId) and ensure the controller interface or its
implementation is annotated with `@Validated` so the constraint is enforced at
runtime; update the method signature for removeFavoriteLocker to include the
validation annotation on lockerId and confirm the class LockerFavoriteApi (or
the implementing controller) has `@Validated`.
- Around line 70-76: The getFavoriteLockerStatus API method accepts a path
variable lockerId without validation; add a Jakarta Bean Validation constraint
(e.g., annotate the lockerId parameter with `@Positive`) and import
jakarta.validation.constraints.Positive so negative or zero IDs are rejected at
the API layer; update the method signature for getFavoriteLockerStatus to
include the annotation on the `@PathVariable` Long lockerId and ensure the class
is set up to trigger validation (e.g., controller is validated by the
framework).
- Around line 102-108: The lockerId path variable in
LockerFavoriteApi.addFavoriteLocker lacks validation and can receive 0 or
negative values; add a validation annotation (e.g., `@Min`(1) or `@Positive`) to the
`@PathVariable` Long lockerId parameter and ensure the controller interface/class
is annotated with `@Validated` so Bean Validation runs (also add the corresponding
javax.validation import). This enforces lockerId > 0 for the addFavoriteLocker
endpoint.

In `@src/main/java/com/zimdugo/locker/entrypoint/LockerFavoriteController.java`:
- Around line 84-90: extractUserId에서 authentication.getName()이 Long으로 파싱되지 않을 경우
NumberFormatException이 발생하므로 Long.valueOf(...) 호출을 try-catch로 감싸고
NumberFormatException을 잡아 적절한 BusinessException으로 변환해 던지세요 (예: 새로운
ErrorCode.INVALID_USER_ID를 추가하거나 기존 ErrorCode.AUTHENTICATED_USER_NOT_FOUND를
재사용). 대상 메서드: extractUserId(Authentication authentication); 에러 변환 시 원인(cause)과
설명 메시지를 포함하도록 하고 필요하면 로깅을 추가해 디버깅 정보를 남기세요.

In
`@src/main/java/com/zimdugo/locker/infrastructure/UserLockerFavoriteRepository.java`:
- Line 33: The query method findTopByUserIdOrderByDisplayOrderDesc returns the
max displayOrder including deleted favorites; update the repository query to
exclude deleted lockers/favorites by adding the appropriate predicate (e.g.,
change to findTopByUserIdAndLockerDeletedFalseOrderByDisplayOrderDesc or
findTopByUserIdAndDeletedFalseOrderByDisplayOrderDesc depending on the entity
field name) so only active favorites are considered when computing new
displayOrder; ensure the repository method references the correct boolean flag
on UserLockerFavoriteEntity (lockerDeleted or deleted) and update any
callers/tests accordingly.

---

Nitpick comments:
In
`@src/main/java/com/zimdugo/locker/infrastructure/FavoriteLockerProperties.java`:
- Around line 10-14: Add range validation to the DefaultOrigin record by
annotating its latitude and longitude components with appropriate Bean
Validation constraints (latitude between -90 and 90, longitude between -180 and
180) and ensure the enclosing configuration properties class is validated at
startup (e.g., annotate the configuration properties class with `@Validated` or
use `@ConfigurationPropertiesBinding` as appropriate) so invalid settings fail
fast; update DefaultOrigin (the record) to use `@DecimalMin/`@DecimalMax or
equivalent annotations on the latitude and longitude parameters and enable
validation on the containing properties bean.

In
`@src/main/java/com/zimdugo/locker/infrastructure/FavoriteLockerStoreAdapter.java`:
- Around line 50-78: The reorder method in FavoriteLockerStoreAdapter currently
throws IllegalArgumentException for various validation failures; change these to
throw BusinessException with a specific ErrorCode (e.g., INVALID_FAVORITE_ORDER
or a new code that fits your error catalog) so it matches the add() behavior;
replace each IllegalArgumentException in reorder(...) with new
BusinessException(ErrorCode.YOUR_CHOICE, "descriptive message") (for
duplicate/unknown/count-mismatch cases), and ensure any callers/tests and the
global exception mapping expect that ErrorCode.

In
`@src/main/java/com/zimdugo/locker/infrastructure/persistence/LockerEntity.java`:
- Around line 35-48: Add a global soft-delete filter to LockerEntity by
annotating the entity class (LockerEntity) with Hibernate's `@Where`(clause =
"deleted = false") so all queries automatically exclude soft-deleted rows; keep
the existing deleted field and markDeleted() method but remove ad-hoc "deleted =
false" checks from repositories; also consider adding `@SQLDelete` and `@Loader` or
a repository method using Session.enableFilter()/native query when you must
explicitly load deleted records.

In
`@src/main/java/com/zimdugo/locker/infrastructure/persistence/UserLockerFavoriteEntity.java`:
- Around line 65-68: The onCreate `@PrePersist` in UserLockerFavoriteEntity sets
createdAt with LocalDateTime.now(), which uses the JVM timezone; change the
createdAt field to an Instant (or offset-aware type) and set it using
Instant.now() in the onCreate method (or switch to database-side
CURRENT_TIMESTAMP by marking the column default), ensuring consistent UTC
timestamps; update any getters/setters/DTO mappings that reference createdAt to
handle Instant accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ee67e00f-671a-4ad5-ad56-ee1c447bf6cd

📥 Commits

Reviewing files that changed from the base of the PR and between 553e774 and 0f6c479.

📒 Files selected for processing (28)
  • src/main/java/com/zimdugo/locker/application/FavoriteLockerCommandService.java
  • src/main/java/com/zimdugo/locker/application/FavoriteLockerItemResponse.java
  • src/main/java/com/zimdugo/locker/application/FavoriteLockerQueryService.java
  • src/main/java/com/zimdugo/locker/application/FavoriteLockerResponse.java
  • src/main/java/com/zimdugo/locker/application/FavoriteLockerStatusResponse.java
  • src/main/java/com/zimdugo/locker/domain/FavoriteLocker.java
  • src/main/java/com/zimdugo/locker/domain/FavoriteLockerPage.java
  • src/main/java/com/zimdugo/locker/domain/FavoriteLockerReader.java
  • src/main/java/com/zimdugo/locker/domain/FavoriteLockerStore.java
  • src/main/java/com/zimdugo/locker/entrypoint/FavoriteLockerOrderUpdateRequest.java
  • src/main/java/com/zimdugo/locker/entrypoint/LockerFavoriteApi.java
  • src/main/java/com/zimdugo/locker/entrypoint/LockerFavoriteController.java
  • src/main/java/com/zimdugo/locker/infrastructure/FavoriteLockerProperties.java
  • src/main/java/com/zimdugo/locker/infrastructure/FavoriteLockerReaderAdapter.java
  • src/main/java/com/zimdugo/locker/infrastructure/FavoriteLockerStoreAdapter.java
  • src/main/java/com/zimdugo/locker/infrastructure/LockerReportLatestUpdateProjection.java
  • src/main/java/com/zimdugo/locker/infrastructure/LockerReportRepository.java
  • src/main/java/com/zimdugo/locker/infrastructure/LockerReportStoreAdapter.java
  • src/main/java/com/zimdugo/locker/infrastructure/LockerRepository.java
  • src/main/java/com/zimdugo/locker/infrastructure/LockerStoreAdapter.java
  • src/main/java/com/zimdugo/locker/infrastructure/UserLockerFavoriteRepository.java
  • src/main/java/com/zimdugo/locker/infrastructure/persistence/LockerEntity.java
  • src/main/java/com/zimdugo/locker/infrastructure/persistence/UserLockerFavoriteEntity.java
  • src/main/resources/application.yaml
  • src/test/java/com/zimdugo/locker/entrypoint/LockerFavoriteControllerTest.java
  • src/test/java/com/zimdugo/locker/infrastructure/FavoriteLockerReaderAdapterTest.java
  • src/test/java/com/zimdugo/locker/infrastructure/LockerReportRepositoryTest.java
  • src/test/java/com/zimdugo/locker/infrastructure/UserLockerFavoriteRepositoryTest.java

Comment thread src/main/java/com/zimdugo/locker/entrypoint/LockerFavoriteApi.java
Comment thread src/main/java/com/zimdugo/locker/entrypoint/LockerFavoriteApi.java
Comment thread src/main/java/com/zimdugo/locker/entrypoint/LockerFavoriteApi.java
Comment thread src/main/java/com/zimdugo/locker/infrastructure/UserLockerFavoriteRepository.java Outdated
Comment thread src/main/java/com/zimdugo/locker/infrastructure/UserLockerFavoriteRepository.java Outdated
Comment thread src/main/java/com/zimdugo/locker/infrastructure/UserLockerFavoriteRepository.java Outdated
Comment thread src/main/java/com/zimdugo/locker/infrastructure/LockerReportStoreAdapter.java Outdated
Comment thread src/main/java/com/zimdugo/locker/infrastructure/FavoriteLockerStoreAdapter.java Outdated
Comment thread src/main/java/com/zimdugo/locker/infrastructure/FavoriteLockerStoreAdapter.java Outdated
);
}

private Long calculateDistanceMeters(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

현재 postGIS 방식을 쓰고 있는데, 따로 계산을 하는 의도가 어떤것인지 궁금합니다.

Comment thread src/main/java/com/zimdugo/locker/infrastructure/FavoriteLockerStoreAdapter.java Outdated
@RequiredArgsConstructor
public class FavoriteLockerReaderAdapter implements FavoriteLockerReader {

private static final double EARTH_RADIUS_METERS = 6_371_000;
Copy link
Copy Markdown
Collaborator

@mike7643 mike7643 May 15, 2026

Choose a reason for hiding this comment

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

아시다시피 하버사인 계산 방식으로 저희는 구현하고 있지 않은데, 지구 반지름을 사용하시는 이유가 궁금합니다

Copy link
Copy Markdown

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

🧹 Nitpick comments (1)
src/test/java/com/zimdugo/locker/entrypoint/LockerFavoriteControllerTest.java (1)

71-104: ⚡ Quick win

위치 파라미터 누락 케이스도 목록 API 테스트에 추가해 주세요.

핵심 정책(사용자 위치 없을 때 fallback 좌표 사용) 대비, 현재는 lat/lng가 있는 요청만 검증하고 있어 회귀를 놓칠 수 있습니다. lat/lng 없이도 200이 반환되고 서비스가 null(또는 컨트롤러 계약값)로 호출되는 케이스를 추가하는 게 안전합니다.

테스트 추가 예시
+    `@Test`
+    `@DisplayName`("위치 파라미터 없이도 즐겨찾기 목록을 조회한다")
+    void getMyFavoriteLockersWithoutLocationReturnsOk() throws Exception {
+        given(favoriteLockerQueryService.getFavorites(1L, 0, 20, null, null))
+            .willReturn(new FavoriteLockerResponse(0, 0, 20, false, List.of()));
+
+        mockMvc.perform(get("/api/v1/me/favorite-lockers")
+                .principal(authenticatedUser()))
+            .andExpect(status().isOk())
+            .andExpect(jsonPath("$.code").value("S200"));
+
+        verify(favoriteLockerQueryService).getFavorites(1L, 0, 20, null, null);
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/zimdugo/locker/entrypoint/LockerFavoriteControllerTest.java`
around lines 71 - 104, Add a new test in LockerFavoriteControllerTest that
verifies the controller behavior when lat/lng are omitted: mock
favoriteLockerQueryService.getFavorites(...) for the authenticated user with the
expected fallback coordinates (or nulls per controller contract) — e.g.
given(favoriteLockerQueryService.getFavorites(1L, 0, 20, null, null)) or the
fallback values used by the controller — and return a FavoriteLockerResponse;
then call
mockMvc.perform(get("/api/v1/me/favorite-lockers").principal(authenticatedUser()))
(no lat/lng params) and assert status isOk and the same response body assertions
as getMyFavoriteLockersReturnsOk so the request without location is covered.
Ensure the new test references getFavorites and LockerFavoriteControllerTest so
the mock setup matches the controller's call contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@src/test/java/com/zimdugo/locker/entrypoint/LockerFavoriteControllerTest.java`:
- Around line 71-104: Add a new test in LockerFavoriteControllerTest that
verifies the controller behavior when lat/lng are omitted: mock
favoriteLockerQueryService.getFavorites(...) for the authenticated user with the
expected fallback coordinates (or nulls per controller contract) — e.g.
given(favoriteLockerQueryService.getFavorites(1L, 0, 20, null, null)) or the
fallback values used by the controller — and return a FavoriteLockerResponse;
then call
mockMvc.perform(get("/api/v1/me/favorite-lockers").principal(authenticatedUser()))
(no lat/lng params) and assert status isOk and the same response body assertions
as getMyFavoriteLockersReturnsOk so the request without location is covered.
Ensure the new test references getFavorites and LockerFavoriteControllerTest so
the mock setup matches the controller's call contract.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 91be4d6a-6728-4573-ab75-d925b2af1bed

📥 Commits

Reviewing files that changed from the base of the PR and between 0f6c479 and 65f3f08.

📒 Files selected for processing (10)
  • src/main/java/com/zimdugo/common/openapi/config/OpenApiConfig.java
  • src/main/java/com/zimdugo/core/exception/ErrorCode.java
  • src/main/java/com/zimdugo/locker/entrypoint/FavoriteLockerOrderUpdateRequest.java
  • src/main/java/com/zimdugo/locker/entrypoint/LockerFavoriteApi.java
  • src/main/java/com/zimdugo/locker/entrypoint/LockerFavoriteController.java
  • src/main/java/com/zimdugo/locker/infrastructure/FavoriteLockerStoreAdapter.java
  • src/main/java/com/zimdugo/locker/infrastructure/LockerReportStoreAdapter.java
  • src/main/java/com/zimdugo/locker/infrastructure/UserLockerFavoriteRepository.java
  • src/test/java/com/zimdugo/locker/entrypoint/LockerFavoriteControllerTest.java
  • src/test/java/com/zimdugo/locker/infrastructure/UserLockerFavoriteRepositoryTest.java
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/main/java/com/zimdugo/locker/entrypoint/FavoriteLockerOrderUpdateRequest.java
  • src/main/java/com/zimdugo/locker/infrastructure/LockerReportStoreAdapter.java
  • src/main/java/com/zimdugo/locker/infrastructure/UserLockerFavoriteRepository.java
  • src/test/java/com/zimdugo/locker/infrastructure/UserLockerFavoriteRepositoryTest.java
  • src/main/java/com/zimdugo/locker/entrypoint/LockerFavoriteController.java
  • src/main/java/com/zimdugo/locker/infrastructure/FavoriteLockerStoreAdapter.java
  • src/main/java/com/zimdugo/locker/entrypoint/LockerFavoriteApi.java

@buddle031
Copy link
Copy Markdown
Collaborator Author

locker 조회 실패는 LOCKER_NOT_FOUND로 구분했고 reorder 과정의 일반 예외도 BusinessException(ErrorCode.BAD_REQUEST)로 정리했습니다. 누락된 import도 함께 보완하고 로컬 Swagger 실행 시 현재 접속한 서버 기준으로 요청이 나가도록 설정도 수정했습니다.

@buddle031
Copy link
Copy Markdown
Collaborator Author

deleted = false 조건을 메서드명에 계속 드러내지 않게 active 조회 의도를 이름으로 드러내는 @query 기반 메서드로 정리했습니다.
엔티티 전역의 @where 방식은 관리자/히스토리 조회처럼 삭제 데이터 접근이 필요한 경우까지 기본적으로 숨길 수 있어 이번에는 사용하지 않고 repository 내부 쿼리로 조건을 모으는 방향으로 구현했습니다.

@buddle031
Copy link
Copy Markdown
Collaborator Author

즐겨찾기 목록 거리 계산도 nearby 조회와 동일하게 PostGIS 기반으로 정리하고 하버사인 계산은 제거했습니다.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants