[FEAT] 푸드트럭 목록 조회 API 구현#31
Conversation
Walkthrough푸드트럭 목록 조회 기능을 추가하고, QueryDSL 기반 검색 인프라를 도입했습니다. 도메인 모델 패키지를 재구성하고 상태 enum을 신설했습니다. 검색 요청/응답 DTO와 변환기, 전역 Enum 값 변환/에러코드 및 예외 처리 로직을 추가/정비했습니다. 예약 상태 직렬화와 문구 일원화도 포함됩니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant API as FoodTruckController
participant App as FoodTruckService
participant Search as FoodTruckSearchService
participant Repo as FoodTruckRepository (QD)
participant Q as QueryDSL
Client->>API: GET /food-trucks?{query}
API->>App: getFoodTrucks(request)
App->>Search: getFoodTrucks(request)
Search->>Repo: getFoodTrucks(request)
Repo->>Q: 동적 where + 커서 페이징 + 정렬
Q-->>Repo: Slice<FoodTruck>
Repo-->>Search: Slice<FoodTruck>
Search->>Search: map -> FoodTruckResponse, build CursorPagingResponse
Search-->>App: CursorPagingResponse<FoodTruckResponse>
App-->>API: CursorPagingResponse
API-->>Client: BaseResponse<CursorPagingResponse>
note over Repo,Q: 상태 ON 필터 포함
sequenceDiagram
autonumber
actor Client
participant Spring as WebBinder
participant Conv as EnumValueConverter
participant Ctrl as Any Controller
participant GEH as GlobalExceptionHandler
Client->>Spring: 요청 파라미터/바디 전달(문자열 값)
Spring->>Conv: String → EnumValue 변환
alt 매칭 성공
Conv-->>Spring: EnumValue
Spring-->>Ctrl: 바인딩 완료
else 매칭 실패
Conv-->>Spring: throw DomainRuleException(API_INVALID_PARAM)
Spring->>GEH: 예외 위임
GEH-->>Client: 에러 응답(API_INVALID_PARAM)
end
Client->>Spring: JSON Enum 역직렬화 실패
Spring->>GEH: HttpMessageNotReadableException
alt InvalidFormatException for Enum
GEH-->>Client: 에러 응답(INVALID_ENUM_VALUE)
else 기타
GEH-->>Client: 에러 응답(API_INVALID_PARAM)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60–90 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
1f56aec to
8c6ff65
Compare
There was a problem hiding this comment.
Actionable comments posted: 6
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/konkuk/chacall/domain/member/application/foodtruck/SavedFoodTruckService.java (1)
31-53: 트랜잭션 경계 추가 및 DB 복합 유니크 제약 필요 (member_id, food_truck_id)
- 문제: exists → save / find → delete가 분리되어 있어 경쟁 상황에서 중복 레코드 생성 또는 삭제/조회 시 404 발생 가능.
- 검사 결과: SavedFoodTruck 엔티티에 @table(name = "saved_food_trucks")만 존재하며 복합 유니크 제약이 없습니다.
- 조치 (우선순위 높음):
- src/main/java/konkuk/chacall/domain/member/application/foodtruck/SavedFoodTruckService.java — 서비스에 트랜잭션 경계 추가 (메서드/클래스 수준). 예시(diff 동일 적용):
@Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class SavedFoodTruckService { @@ - public SavedFoodTruckStatusResponse updateFoodTruckSaveStatus(UpdateFoodTruckSaveStatusRequest request, Long foodTruckId, User member) { + @Transactional + public SavedFoodTruckStatusResponse updateFoodTruckSaveStatus(UpdateFoodTruckSaveStatusRequest request, Long foodTruckId, User member) {
- src/main/java/konkuk/chacall/domain/member/domain/SavedFoodTruck.java — 엔티티 또는 DB 마이그레이션에 복합 유니크 제약 추가: unique(member_id, food_truck_id).
- 저장 시 레이스로 인한 DB 유니크 위반(DataIntegrityViolationException 등)을 캐치해 BusinessException(ErrorCode.SAVED_FOOD_TRUCK_ALREADY_EXIST)로 변환하도록 처리.
🧹 Nitpick comments (20)
src/main/java/konkuk/chacall/domain/foodtruck/domain/model/AvailableDate.java (2)
22-26: 날짜 범위 유효성 보장 추가 제안(startAt ≤ endAt)엔티티 레벨에서 역전 방지 검증을 걸어두면 실수 입력을 조기에 차단할 수 있습니다.
import java.time.LocalDate; +import jakarta.validation.constraints.AssertTrue; @Entity @Table(name = "available_dates") @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter public class AvailableDate extends BaseEntity { @@ @Column(nullable = false) private LocalDate endAt; @@ private FoodTruck foodTruck; + @AssertTrue(message = "startAt must be on or before endAt") + private boolean isValidDateRange() { + return startAt == null || endAt == null || !startAt.isAfter(endAt); + } }
11-13: FK 인덱스 권장: food_truck_id해당 엔티티는 food_truck_id 기준 삭제/조회가 잦을 가능성이 높습니다. DDL 생성 사용 시 인덱스를 선언해두면 필터/삭제 성능에 이점이 있습니다.
-@Table(name = "available_dates") +@Table( + name = "available_dates", + indexes = { + @Index(name = "idx_available_dates_food_truck_id", columnList = "food_truck_id") + } +)(참고:
jakarta.persistence.Indeximport 필요)src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/AvailableDateRepository.java (1)
11-13: 파생 쿼리 대안(선택): 유지보수 단순화성능상 벌크 JPQL이 필요 없다면 파생 메서드로 단순화할 수 있습니다.
- @Modifying(clearAutomatically = true, flushAutomatically = true) - @Query("DELETE FROM AvailableDate ad WHERE ad.foodTruck.foodTruckId = :foodTruckId") - void deleteAllByFoodTruckId(@Param("foodTruckId") Long foodTruckId); + void deleteByFoodTruck_FoodTruckId(Long foodTruckId);src/main/java/konkuk/chacall/domain/foodtruck/domain/model/Menu.java (1)
1-33: 패키지 이동 LGTM + 칼럼 제약 보완 제안엔티티 이동은 일관성 측면에서 좋습니다. 다음 소소한 매핑 보완을 추천합니다.
- name/description/imageUrl 길이 제약 명시.
- 가격에 소수점이 필요 없으면 Integer 유지, 필요하면 BigDecimal로 전환.
아래처럼 길이 제약만 추가해도 효과적입니다.
- @Column(nullable = false) - private String name; + @Column(nullable = false, length = 100) + private String name; @@ - private String description; + @Column(length = 500) + private String description; @@ - private String imageUrl; + @Column(length = 300) + private String imageUrl;src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/MenuRepository.java (1)
11-13: 벌크 삭제 후 영속성 컨텍스트 동기화 옵션 고려벌크 연산은 1차 캐시와 불일치가 생길 수 있습니다. 안전성을 위해 자동 flush/clear 옵션을 권장합니다.
- @Modifying + @Modifying(clearAutomatically = true, flushAutomatically = true) @Query("DELETE FROM Menu m WHERE m.foodTruck.foodTruckId = :foodTruckId") void deleteAllByFoodTruckId(@Param("foodTruckId") Long foodTruckId);src/main/java/konkuk/chacall/domain/foodtruck/domain/model/FoodTruckServiceArea.java (1)
10-13: FK 조회가 잦은 테이블에 인덱스 추가 권장food_truck_id, region_id 기준 조회가 빈번합니다. 인덱스를 추가해 주세요.
-@Table(name = "food_truck_service_areas") +@Table( + name = "food_truck_service_areas", + indexes = { + @Index(name = "idx_ftsa_food_truck_id", columnList = "food_truck_id"), + @Index(name = "idx_ftsa_region_id", columnList = "region_id") + } +)src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/FoodTruckServiceAreaRepository.java (2)
13-16: 빈 컬렉션 IN 절 처리 확인foodTruckIds가 빈 리스트일 때 JPA 구현별로 IN () SQL이 문제를 일으킬 수 있습니다. 호출부에서 빈 리스트면 쿼리를 생략하는 가드가 안전합니다.
빈 리스트 전달 가능성이 있는 호출부(MyFoodTruckService.getServiceAreaMap)를 점검해 주세요.
18-20: 벌크 삭제 동기화 옵션MenuRepository와 동일하게 벌크 삭제 시 flush/clear 옵션을 주면 1차 캐시 불일치 위험을 줄일 수 있습니다.
- @Modifying + @Modifying(clearAutomatically = true, flushAutomatically = true) @Query("DELETE FROM FoodTruckServiceArea ftsa WHERE ftsa.foodTruck.foodTruckId = :foodTruckId") void deleteAllByFoodTruckId(@Param("foodTruckId") Long foodTruckId);src/main/java/konkuk/chacall/domain/owner/application/myfoodtruck/MyFoodTruckService.java (1)
92-102: 빈 목록 가드로 불필요한 쿼리/에러 회피foodTrucks가 비어있을 때 IN 절 호출을 피하면 낭비와 구현체별 빈 리스트 오류를 예방합니다.
private Map<Long, List<FoodTruckServiceArea>> getServiceAreaMap(List<FoodTruck> foodTrucks) { List<Long> foodTruckIds = foodTrucks.stream() .map(FoodTruck::getFoodTruckId) .toList(); + if (foodTruckIds.isEmpty()) { + return Map.of(); + } + List<FoodTruckServiceArea> serviceAreas = foodTruckServiceAreaRepository .findAllWithRegionByFoodTruckIdIn(foodTruckIds);src/main/java/konkuk/chacall/domain/foodtruck/domain/value/FoodTruckStatus.java (1)
1-14: 상태 전환 로직 고려사항FoodTruckStatus enum이 잘 정의되었습니다. 다만, 향후 상태 전환 비즈니스 로직이 필요할 경우를 대비해 다음 사항들을 고려해보세요:
- 상태 전환 검증 메서드 추가 (예: PENDING → ON/OFF는 가능하지만 ON → PENDING는 불가)
- 상태별 허용 작업 정의
향후 확장을 위해 다음과 같은 메서드 추가를 고려하세요:
public boolean canTransitionTo(FoodTruckStatus newStatus) { return switch (this) { case PENDING -> newStatus == ON || newStatus == OFF; case ON -> newStatus == OFF; case OFF -> newStatus == ON; }; }src/main/java/konkuk/chacall/global/common/converter/DateRangeRequestConverter.java (1)
13-13: 상수 이름 개선 권장
F라는 변수명은 의미가 명확하지 않습니다. 코드 가독성 향상을 위해 더 명시적인 이름을 사용하는 것이 좋습니다.- private static final DateTimeFormatter F = DateTimeFormatter.ofPattern("yyyy.MM.dd"); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy.MM.dd");그리고 아래 코드에서도
F→DATE_FORMATTER로 변경하세요.src/main/java/konkuk/chacall/domain/foodtruck/presentation/FoodTruckController.java (2)
29-33: API 설명 톤 정리 및 개발 메모 제거 권장공개 스펙 문서에 개발 메모(ENUM 맞춤, !!!)가 그대로 노출됩니다. 사용자용 문구로 정리해 주세요.
@Operation( - summary = "푸드트럭 조회", - description = "필터링 조건을 기반으로 푸드트럭을 조회합니다. / 서버와 ENUM 값을 맞춰두고 사용하는 것이 좋을 것 같습니다!!!" + summary = "푸드트럭 조회", + description = "필터링 조건을 기반으로 푸드트럭을 조회합니다. (클라이언트는 서버에서 정의한 ENUM 값과 일치하는 값으로 요청해야 합니다)" )
14-14: @slf4j 미사용 — 제거하거나 실제 로깅 추가현재 로깅을 사용하지 않습니다. 주석/삭제하여 경량화하거나, 요청 파라미터 검증 실패·예외 로깅 등 실사용을 추가하세요.
-import lombok.extern.slf4j.Slf4j; ... -@Slf4jAlso applies to: 24-24
src/main/java/konkuk/chacall/domain/foodtruck/presentation/dto/response/FoodTruckResponse.java (1)
20-29: 대표 사진 null/빈 목록 대비 여부 확인getMainPhotoUrl()이 항상 값 보장을 한다면 OK입니다. 불확실하다면 null/빈 목록 시 기본 이미지로 폴백하거나 null 반환을 명시해 주세요(스웨거 예시/nullable 표기 포함).
src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/querydsl/FoodTruckSearchRepository.java (1)
7-9: 운영/성능 팁: 필터 컬럼 인덱싱 고려
- 지역 startsWith: region_code(혹은 prefix) 인덱스
- 상태(ON), 수량, 결제/전기 여부: 단일/복합 인덱스
- 일정 범위: 시작/종료일 컬럼 인덱스(또는 범위 전용 커버링 인덱스)
src/main/java/konkuk/chacall/domain/foodtruck/domain/value/AvailableQuantity.java (1)
24-24: null 입력 시 empty 반환 계약 재확인 필요현재 null이면 빈 집합을 반환합니다. 상위 호출부(Repository)에서 null일 때 메서드를 호출하지 않으니 문제는 없지만, 다른 곳에서 재사용 시
in(empty)가 “항상 거짓”이 되어 전체 결과를 배제할 수 있습니다. 메서드 주석에 이 계약을 명시하거나, 안전을 위해 “필터 미적용 의도”라면allOf(...)반환도 고려하세요.src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/querydsl/FoodTruckSearchRepositoryImpl.java (4)
49-66: 지역 prefix 조건: 전부 null인 입력 방지 가드모두 null이면 빈 BooleanBuilder가 들어가 예기치 않은 쿼리가 될 수 있습니다. 값이 있을 때만 exists 절을 추가하세요.
적용 예시:
BooleanBuilder anyPrefix = new BooleanBuilder(); for (Long code : request.regionCodes()) { if (code == null) continue; anyPrefix.or( region.regionCode.stringValue().startsWith(code.toString()) ); } - BooleanExpression regionMatch = JPAExpressions.selectOne() - .from(foodTruckServiceArea) - .join(foodTruckServiceArea.region, region) - .where( - foodTruckServiceArea.foodTruck.eq(foodTruck), - anyPrefix - ) - .exists(); - where.and(regionMatch); + if (anyPrefix.hasValue()) { + BooleanExpression regionMatch = JPAExpressions.selectOne() + .from(foodTruckServiceArea) + .join(foodTruckServiceArea.region, region) + .where(foodTruckServiceArea.foodTruck.eq(foodTruck), anyPrefix) + .exists(); + where.and(regionMatch); + }
95-101: 카테고리 null 입력 방어
categories에 null이 섞이면 잘못된 LIKE 상수가 생성될 수 있습니다. null은 건너뛰세요.적용 예시:
- for (MenuCategory category : request.categories()) { + for (MenuCategory category : request.categories()) { + if (category == null) continue; any.or(Expressions.booleanTemplate(
92-103: 문자열 LIKE 기반 카테고리 매칭의 인덱스 비효율
concat(',', col, ',') like '%,val,%'패턴은 인덱스를 타기 어렵습니다. 장기적으로는
- 정규화: 트럭-카테고리 다대다 조인 테이블로 전환
- 또는 JSON 배열 컬럼 + GIN/FTS 인덱스(지원 DB 한정)
을 검토하세요.
41-46: lower() 사용 시 인덱스 영향 고려DB가 기본적으로 대소문자 비민감(collation)이라면 lower()를 제거하고 LIKE만 사용하는 편이 인덱스 친화적일 수 있습니다. 그렇지 않다면 함수 인덱스 생성도 대안입니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (35)
build.gradle(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/application/FoodTruckService.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/application/search/FoodTruckSearchService.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/model/AvailableDate.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/model/FoodTruck.java(2 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/model/FoodTruckServiceArea.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/model/Menu.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/AvailableDateRepository.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/FoodTruckRepository.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/FoodTruckServiceAreaRepository.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/MenuRepository.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/querydsl/FoodTruckSearchRepository.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/querydsl/FoodTruckSearchRepositoryImpl.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/value/AvailableQuantity.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/value/FoodTruckStatus.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/presentation/FoodTruckController.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/presentation/dto/request/DateRangeRequest.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/presentation/dto/request/FoodTruckSearchRequest.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/presentation/dto/response/FoodTruckResponse.java(1 hunks)src/main/java/konkuk/chacall/domain/member/application/foodtruck/SavedFoodTruckService.java(1 hunks)src/main/java/konkuk/chacall/domain/member/application/rating/RatingService.java(1 hunks)src/main/java/konkuk/chacall/domain/member/domain/Rating.java(1 hunks)src/main/java/konkuk/chacall/domain/member/domain/SavedFoodTruck.java(1 hunks)src/main/java/konkuk/chacall/domain/member/domain/repository/RatingRepository.java(1 hunks)src/main/java/konkuk/chacall/domain/member/domain/repository/SavedFoodTruckRepository.java(1 hunks)src/main/java/konkuk/chacall/domain/member/presentation/dto/response/MemberReservationDetailResponse.java(1 hunks)src/main/java/konkuk/chacall/domain/member/presentation/dto/response/MemberReservationHistoryResponse.java(1 hunks)src/main/java/konkuk/chacall/domain/member/presentation/dto/response/ReservationForRatingResponse.java(1 hunks)src/main/java/konkuk/chacall/domain/member/presentation/dto/response/SavedFoodTruckResponse.java(1 hunks)src/main/java/konkuk/chacall/domain/owner/application/myfoodtruck/MyFoodTruckService.java(1 hunks)src/main/java/konkuk/chacall/domain/owner/presentation/dto/response/MyFoodTruckResponse.java(1 hunks)src/main/java/konkuk/chacall/domain/reservation/application/reservationinfo/ReservationInfoService.java(1 hunks)src/main/java/konkuk/chacall/domain/reservation/domain/model/Reservation.java(1 hunks)src/main/java/konkuk/chacall/global/common/converter/DateRangeRequestConverter.java(1 hunks)src/main/java/konkuk/chacall/global/config/QuerydslConfig.java(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (21)
src/main/java/konkuk/chacall/domain/member/domain/SavedFoodTruck.java (2)
src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruck.java (1)
Builder(16-98)src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruckServiceArea.java (1)
Entity(10-28)
src/main/java/konkuk/chacall/domain/foodtruck/domain/model/FoodTruckServiceArea.java (1)
src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruck.java (2)
Builder(16-98)serviceArea(95-95)
build.gradle (1)
src/main/java/konkuk/chacall/global/common/domain/BaseEntity.java (1)
Getter(11-27)
src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/MenuRepository.java (1)
src/main/java/konkuk/chacall/domain/foodtruck/domain/Menu.java (1)
Entity(9-33)
src/main/java/konkuk/chacall/domain/member/presentation/dto/response/ReservationForRatingResponse.java (2)
src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruck.java (1)
Builder(16-98)src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruckServiceArea.java (1)
Entity(10-28)
src/main/java/konkuk/chacall/domain/member/presentation/dto/response/SavedFoodTruckResponse.java (2)
src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruck.java (1)
Builder(16-98)src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruckServiceArea.java (1)
Entity(10-28)
src/main/java/konkuk/chacall/domain/foodtruck/presentation/dto/response/FoodTruckResponse.java (1)
src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruck.java (1)
Builder(16-98)
src/main/java/konkuk/chacall/domain/foodtruck/presentation/FoodTruckController.java (3)
src/main/java/konkuk/chacall/domain/foodtruck/application/FoodTruckService.java (1)
RequiredArgsConstructor(11-21)src/main/java/konkuk/chacall/domain/member/presentation/MemberController.java (2)
Operation(42-53)Operation(28-40)src/main/java/konkuk/chacall/domain/owner/presentation/OwnerController.java (1)
Operation(172-184)
src/main/java/konkuk/chacall/global/common/converter/DateRangeRequestConverter.java (4)
src/main/java/konkuk/chacall/global/common/converter/ReservationDateListConverter.java (3)
Converter(20-54)Override(33-53)Override(27-31)src/main/java/konkuk/chacall/domain/reservation/domain/value/ReservationInfo.java (2)
date(66-66)getFormattedDateInfos(62-68)src/main/java/konkuk/chacall/domain/reservation/domain/value/DateRange.java (1)
Embeddable(10-26)src/main/java/konkuk/chacall/domain/reservation/domain/value/ReservationDateList.java (2)
AllArgsConstructor(17-82)JsonCreator(36-55)
src/main/java/konkuk/chacall/domain/foodtruck/application/search/FoodTruckSearchService.java (1)
src/main/java/konkuk/chacall/domain/foodtruck/application/FoodTruckService.java (1)
RequiredArgsConstructor(11-21)
src/main/java/konkuk/chacall/domain/foodtruck/presentation/dto/request/DateRangeRequest.java (3)
src/main/java/konkuk/chacall/domain/reservation/domain/value/DateRange.java (1)
Embeddable(10-26)src/main/java/konkuk/chacall/domain/foodtruck/domain/AvailableDate.java (1)
Entity(11-32)src/main/java/konkuk/chacall/domain/reservation/domain/value/ReservationDateList.java (1)
AllArgsConstructor(17-82)
src/main/java/konkuk/chacall/domain/foodtruck/domain/model/AvailableDate.java (2)
src/main/java/konkuk/chacall/domain/foodtruck/domain/AvailableDate.java (1)
Entity(11-32)src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruckServiceArea.java (1)
Entity(10-28)
src/main/java/konkuk/chacall/domain/foodtruck/domain/model/FoodTruck.java (2)
src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruckServiceArea.java (1)
Entity(10-28)src/main/java/konkuk/chacall/domain/foodtruck/domain/Menu.java (1)
Entity(9-33)
src/main/java/konkuk/chacall/global/config/QuerydslConfig.java (1)
src/main/java/konkuk/chacall/global/config/JpaAuditingConfig.java (1)
Configuration(6-9)
src/main/java/konkuk/chacall/domain/reservation/application/reservationinfo/ReservationInfoService.java (3)
src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruck.java (1)
Builder(16-98)src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruckServiceArea.java (1)
Entity(10-28)src/main/java/konkuk/chacall/domain/foodtruck/domain/Menu.java (1)
Entity(9-33)
src/main/java/konkuk/chacall/domain/member/domain/repository/SavedFoodTruckRepository.java (2)
src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruck.java (1)
Builder(16-98)src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruckServiceArea.java (1)
Entity(10-28)
src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/AvailableDateRepository.java (1)
src/main/java/konkuk/chacall/domain/foodtruck/domain/AvailableDate.java (1)
Entity(11-32)
src/main/java/konkuk/chacall/domain/foodtruck/application/FoodTruckService.java (1)
src/main/java/konkuk/chacall/domain/foodtruck/application/search/FoodTruckSearchService.java (1)
Service(14-34)
src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/FoodTruckServiceAreaRepository.java (2)
src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruckServiceArea.java (1)
Entity(10-28)src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruck.java (1)
Builder(16-98)
src/main/java/konkuk/chacall/domain/owner/application/myfoodtruck/MyFoodTruckService.java (2)
src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruck.java (2)
Builder(16-98)serviceArea(95-95)src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruckServiceArea.java (1)
Entity(10-28)
src/main/java/konkuk/chacall/domain/member/presentation/dto/response/MemberReservationDetailResponse.java (1)
src/main/java/konkuk/chacall/domain/member/application/reservation/MemberReservationService.java (2)
reservation(33-33)getMemberReservationDetail(39-46)
🔇 Additional comments (29)
src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/AvailableDateRepository.java (2)
3-3: import 경로 업데이트 적절 — 전역 검색 재확인 필요도메인 이동(model)과 일치합니다. 구 경로 참조가 남아있지 않은지 전역 검색으로 재확인하세요.
# 저장소 루트에서 실행 rg -n --hidden -S 'import\s+konkuk\.chacall\.domain\.foodtruck\.domain\.AvailableDate' -C2 rg -n --hidden -S 'konkuk\.chacall\.domain\.foodtruck\.domain' -C2
11-13: 벌크 삭제: @Modifying에 clearAutomatically/flushAutomatically 추가 권장벌크 연산은 영속성 컨텍스트(1차 캐시)와 동기화되지 않아 stale 엔티티가 남을 수 있습니다. 자동 flush/clear를 켜서 동기화 문제를 방지하세요.
- @Modifying + @Modifying(clearAutomatically = true, flushAutomatically = true) @Query("DELETE FROM AvailableDate ad WHERE ad.foodTruck.foodTruckId = :foodTruckId") void deleteAllByFoodTruckId(@Param("foodTruckId") Long foodTruckId);
- 호출부 확인: src/main/java/konkuk/chacall/domain/owner/application/myfoodtruck/MyFoodTruckService.java (availableDateRepository.deleteAllByFoodTruckId(...) 등) — 해당 서비스 메서드가 쓰기 트랜잭션(@transactional)으로 감싸여 있는지 확인하세요.
- 동일 패턴의 다른 Repository에도 동일 변경 권장: MenuRepository, FoodTruckServiceAreaRepository, ReservationRepository, RatingRepository, SavedFoodTruckRepository.
src/main/java/konkuk/chacall/domain/foodtruck/domain/model/AvailableDate.java (1)
1-1: 패키지 이동 확인 완료 — 컴파일 문제 없음FoodTruck.java와 AvailableDate.java가 konkuk.chacall.domain.foodtruck.domain.model 패키지에 위치하며, 구 패키지 참조(예: QAvailableDate)는 발견되지 않았습니다.
src/main/java/konkuk/chacall/domain/owner/application/myfoodtruck/MyFoodTruckService.java (2)
39-51: 페이지 응답 구성 LGTMSlice 기반 조회 → 배치 로딩 → DTO 매핑 흐름이 명확합니다.
53-80: @transactional 추가 필요 — 연쇄 삭제에서 원자성 보장파일: src/main/java/konkuk/chacall/domain/owner/application/myfoodtruck/MyFoodTruckService.java (Lines 53–80)
여러 엔티티를 순차적으로 물리 삭제하므로 중간 실패 시 부분 삭제로 데이터 무결성이 깨질 수 있습니다. 메서드에 @transactional을 적용해 원자성을 보장하세요.
import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @@ - public void deleteMyFoodTruck(Long ownerId, Long foodTruckId) { + @Transactional + public void deleteMyFoodTruck(Long ownerId, Long foodTruckId) {소프트 삭제(예: BaseEntity.status) 정책이 있다면 물리 삭제가 정책에 부합하는지 확인.
build.gradle (1)
61-65: QueryDSL: JPAQueryFactory 빈은 확인되나 Q 클래스 생성·테스트 APT 추가 확인 필요
- JPAQueryFactory 빈 확인: src/main/java/konkuk/chacall/global/config/QuerydslConfig.java.
- QueryDSL 사용(정적 Q import) 확인: src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/querydsl/FoodTruckSearchRepositoryImpl.java (QFoodTruck, QAvailableDate, QRegion 등).
- 조치(요구): 로컬/CI에서 gradle 빌드로 Q 클래스가 build/generated/sources/annotationProcessor/...에 생성되어 컴파일 소스에 포함되는지 확인. 테스트에서 QueryDSL을 직접 사용하면 build.gradle에 testAnnotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"(또는 동일한 APT 설정) 추가.
src/main/java/konkuk/chacall/domain/reservation/application/reservationinfo/ReservationInfoService.java (1)
58-81: @transactional 누락 — updateReservation 변경사항이 영속되지 않을 위험ReservationInfoService에 클래스 레벨 @transactional(readOnly = true)를 추가하고, 쓰기 메서드(createReservation, updateReservation)에 @transactional을 명시하세요. 현재 클래스에 @transactional이 없어 reservation.update(...)에 의존한 더티체킹이 트랜잭션 없이 flush/commit되지 않을 수 있습니다. 호출자가 항상 트랜잭션을 보장하는지(예: ReservationService의 쓰기 메서드에서만 호출되는지) 확인하십시오.
@@ -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @@ -@Service -@RequiredArgsConstructor -public class ReservationInfoService { +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ReservationInfoService { @@ - public Long createReservation(CreateReservationRequest request, User owner, User member) { + @Transactional + public Long createReservation(CreateReservationRequest request, User owner, User member) { @@ - public Long updateReservation(Long reservationId, UpdateReservationRequest request, User user) { + @Transactional + public Long updateReservation(Long reservationId, UpdateReservationRequest request, User user) {src/main/java/konkuk/chacall/domain/member/presentation/dto/response/SavedFoodTruckResponse.java (1)
4-4: 패키지 구조 개선 적용 승인FoodTruck 엔티티를
domain.model패키지로 이동한 변경사항이 일관되게 적용되었습니다. 도메인 모델의 명확한 계층 구조를 통해 코드 조직화가 개선되었습니다.src/main/java/konkuk/chacall/domain/member/application/rating/RatingService.java (1)
3-3: 패키지 리팩토링 승인FoodTruck import 경로가 새로운
domain.model패키지 구조에 맞게 올바르게 업데이트되었습니다.src/main/java/konkuk/chacall/domain/reservation/domain/model/Reservation.java (1)
5-5: import 경로 업데이트 적절함FoodTruck 도메인 모델의 새로운 패키지 위치에 맞춘 import 변경이 정확합니다.
src/main/java/konkuk/chacall/domain/member/domain/Rating.java (1)
4-4: 도메인 모델 패키지 이동 반영 완료FoodTruck 엔티티의 새로운 패키지 경로가 정확하게 반영되었습니다.
src/main/java/konkuk/chacall/domain/member/domain/SavedFoodTruck.java (1)
4-4: 일관된 패키지 구조 적용프로젝트 전반의 FoodTruck 엔티티 패키지 이동에 맞춘 import 변경이 적절하게 이루어졌습니다.
src/main/java/konkuk/chacall/domain/owner/presentation/dto/response/MyFoodTruckResponse.java (1)
4-5: 패키지 구조 개선 및 불필요한 import 제거FoodTruck과 FoodTruckServiceArea의 새로운
domain.model패키지 경로로의 업데이트가 정확하며, 사용하지 않는Collectorsimport가 적절히 제거되었습니다.src/main/java/konkuk/chacall/domain/member/domain/repository/SavedFoodTruckRepository.java (1)
3-3: 리포지토리 인터페이스 import 업데이트 승인FoodTruck 도메인 모델의 새로운 패키지 위치에 따른 import 변경이 올바르게 적용되었습니다.
src/main/java/konkuk/chacall/domain/member/presentation/dto/response/MemberReservationHistoryResponse.java (1)
4-4: DTO import 경로 업데이트 완료FoodTruck 엔티티의 패키지 이동에 따른 import 변경이 정확히 반영되었습니다.
src/main/java/konkuk/chacall/domain/member/presentation/dto/response/ReservationForRatingResponse.java (1)
4-4: 패키지 이동이 올바르게 적용되었습니다FoodTruck 도메인 모델의 패키지 이동(
domain→domain.model)이 정확히 반영되었습니다.src/main/java/konkuk/chacall/global/config/QuerydslConfig.java (1)
12-18: QueryDSL 설정이 적절합니다JPAQueryFactory 빈 설정이 올바르게 구현되었습니다. EntityManager를 주입받아 QueryDSL 사용을 위한 기본 설정이 잘 되어 있습니다.
src/main/java/konkuk/chacall/domain/member/presentation/dto/response/MemberReservationDetailResponse.java (1)
4-4: 패키지 이동에 따른 import 정정 — 적절합니다도메인 모델 이동(domain.model)에 맞춘 import 변경이 일관적입니다.
src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/querydsl/FoodTruckSearchRepository.java (1)
7-9: QueryDSL 확장 인터페이스 설계 적절검색 요구사항을 감싸는 전용 리포지토리 분리는 좋습니다.
src/main/java/konkuk/chacall/domain/member/domain/repository/RatingRepository.java (1)
3-3: FoodTruck import 경로 변경 — 문제 없습니다src/main/java/konkuk/chacall/domain/foodtruck/application/FoodTruckService.java (1)
16-20: 단순 위임 계층 — 적절합니다읽기 전용 트랜잭션, 역할 분리가 명확합니다.
src/main/java/konkuk/chacall/domain/foodtruck/application/search/FoodTruckSearchService.java (2)
23-25: JDK 호환성 점검 (Stream.toList()) — 확인됨
build.gradle의 Gradle toolchain이 Java 17로 설정되어 있어 Stream.toList() 사용에 문제가 없습니다 (build.gradle:12–13).
20-32: 확인: 커서 페이징 정합성 문제 없음
FoodTruckSearchRepositoryImpl.java에서 foodTruck.foodTruckId.lt(lastCursor)와 .orderBy(foodTruck.foodTruckId.desc())를 사용하므로, FoodTruckResponse.foodTruckId를 커서로 사용하는 로직과 일치합니다.src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/FoodTruckRepository.java (1)
11-19: 초기 페이지 호출 단순화를 위해 lastCursor를 nullable로 허용하세요@Query("SELECT ft FROM FoodTruck ft " + "WHERE ft.owner.userId = :ownerId " + - "AND ft.foodTruckId < :lastCursor " + + "AND (:lastCursor IS NULL OR ft.foodTruckId < :lastCursor) " + "ORDER BY ft.foodTruckId DESC") Slice<FoodTruck> findByOwnerUserIdWithCursor(@Param("ownerId") Long ownerId, @Param("lastCursor") Long lastCursor, Pageable pageable);호출부 점검: src/main/java/konkuk/chacall/domain/owner/application/myfoodtruck/MyFoodTruckService.java:86 — 최초 페이지에서는 lastCursor를 null로 넘기거나 기존 Long.MAX_VALUE 사용을 제거하도록 조정하세요.
src/main/java/konkuk/chacall/domain/foodtruck/domain/value/AvailableQuantity.java (1)
22-38: 수량 필터 포함 규칙 매핑이 요구사항과 일치합니다.작은 임계값 요청일수록 더 큰 용량 선언 값들을 포함하도록 잘 정의됐습니다(예: <100 요청 시 150/200+ 포함, <50는 전체 포함). 단위 테스트로 각 입력값별 결과 집합을 고정(snap)해두면 회귀 방지에 좋겠습니다.
원하시면 JUnit 파라미터라이즈드 테스트 골격을 만들어 드리겠습니다.
src/main/java/konkuk/chacall/domain/foodtruck/presentation/dto/request/DateRangeRequest.java (1)
11-15: @jsonformat은 쿼리 파라미터(@ModelAttribute)에는 영향 없음스케줄은
"yyyy.MM.dd~yyyy.MM.dd"콤마 구분 쿼리 파라미터로 들어옵니다. 이 경우 포맷팅은 컨버터가 담당해야 합니다.DateRangeRequestConverter가Converter<String, DateRangeRequest>로 등록(예: @component 또는 WebMvcConfigurer#addFormatters)되어 있는지 확인 부탁드립니다.원하시면 저장소에서 컨버터 등록 여부를 검사하는 스크립트를 드리겠습니다.
src/main/java/konkuk/chacall/domain/foodtruck/presentation/dto/request/FoodTruckSearchRequest.java (2)
21-27: 콤마 구분 리스트 바인딩 동작 확인(regionCodes, categories)Spring MVC가 단일 쿼리 파라미터
"11680,41"/"SNACK,KOREAN"를 List/List로 분할해주는지(컨버전 서비스 설정) 확인 필요합니다. 일부 설정에서는 반복 파라미터 방식만 지원합니다.원하시면 컨트롤러 시그니처/바인딩 설정을 조회하는 스크립트를 제공하겠습니다.
Also applies to: 37-47
52-54: 페이지 사이즈 상한 및 기본 커서 값 검증
CursorPagingRequest가 size 상한(예: ≤100)과 기본 cursor(첫 페이지용) 값을 보장하는지 확인해주세요. 리포지토리에서 cursor null 가드가 없으면 NPE가 날 수 있습니다.src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/querydsl/FoodTruckSearchRepositoryImpl.java (1)
132-136: Slice 커서 계산 OKsize+1 조회 후 hasNext 계산과 자르기 패턴이 올바릅니다.
| for (DateRangeRequest dateRange : request.schedules()) { | ||
| BooleanExpression covered = JPAExpressions.selectOne() | ||
| .from(availableDate) | ||
| .where(availableDate.foodTruck.foodTruckId.eq(foodTruck.foodTruckId), | ||
| availableDate.startAt.loe(dateRange.startDate()), | ||
| availableDate.endAt.goe(dateRange.endDate())) | ||
| .exists(); | ||
| any.or(covered); | ||
| } |
There was a problem hiding this comment.
스케줄 항목 Null/역전기간 가드 추가
DTO 검증이 실패를 막겠지만, 방어적으로도 NPE와 잘못된 조건을 피하세요.
적용 예시:
- for (DateRangeRequest dateRange : request.schedules()) {
+ for (DateRangeRequest dateRange : request.schedules()) {
+ if (dateRange == null || dateRange.startDate() == null || dateRange.endDate() == null) continue;
+ if (dateRange.endDate().isBefore(dateRange.startDate())) continue;
BooleanExpression covered = JPAExpressions.selectOne()
.from(availableDate)
- .where(availableDate.foodTruck.foodTruckId.eq(foodTruck.foodTruckId),
+ .where(availableDate.foodTruck.foodTruckId.eq(foodTruck.foodTruckId),
availableDate.startAt.loe(dateRange.startDate()),
availableDate.endAt.goe(dateRange.endDate()))
.exists();
any.or(covered);
}📝 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.
| for (DateRangeRequest dateRange : request.schedules()) { | |
| BooleanExpression covered = JPAExpressions.selectOne() | |
| .from(availableDate) | |
| .where(availableDate.foodTruck.foodTruckId.eq(foodTruck.foodTruckId), | |
| availableDate.startAt.loe(dateRange.startDate()), | |
| availableDate.endAt.goe(dateRange.endDate())) | |
| .exists(); | |
| any.or(covered); | |
| } | |
| for (DateRangeRequest dateRange : request.schedules()) { | |
| if (dateRange == null || dateRange.startDate() == null || dateRange.endDate() == null) continue; | |
| if (dateRange.endDate().isBefore(dateRange.startDate())) continue; | |
| BooleanExpression covered = JPAExpressions.selectOne() | |
| .from(availableDate) | |
| .where(availableDate.foodTruck.foodTruckId.eq(foodTruck.foodTruckId), | |
| availableDate.startAt.loe(dateRange.startDate()), | |
| availableDate.endAt.goe(dateRange.endDate())) | |
| .exists(); | |
| any.or(covered); | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/querydsl/FoodTruckSearchRepositoryImpl.java
around lines 72-80, the loop builds a date-range predicate without guarding
against null DateRangeRequest or null start/end dates and against inverted
ranges; update the loop to first skip any null dateRange, skip or reject ranges
where startDate or endDate is null, and skip ranges where startDate is after
endDate (or normalize/flip if your domain requires), then only build the
JPAExpressions.selectOne() covered predicate for valid ranges and combine it
into the BooleanBuilder (preserving mutation/assignment semantics); optionally
add a debug warning when skipping invalid entries.
There was a problem hiding this comment.
p2: 현재 dateRange 내부에서 startDate와 endDate의 날짜 유효성 검증이 안되는 것 같습니다. 사실 로직상 발생하지 않는 경우긴 하지만, 그렇게 들어오는 경우 저희 조회 로직이 꼬여버릴 수도 있어서 코래 말처럼 Querydsl 레벨이나 dto 레벨 둘중 한곳에서는 유효성 검증이 들어가야 할 것 같아요!
dto 레벨 검증은 여기에서 코래가 설명하고 있네요!
There was a problem hiding this comment.
🧩 Analysis chain
I need to check the referenced comment first to understand the DTO level validation discussion.
@buzz0331 좋은 지적이네요! 말씀하신 대로 두 레벨 모두에서 검증을 고려해볼 수 있겠습니다.
추천하는 접근법:
- DTO 레벨 (1차 방어선):
DateRangeRequest에 Bean Validation 어노테이션 추가 - QueryDSL 레벨 (2차 방어선): 방어적 프로그래밍으로 런타임 안전성 확보
DTO 레벨 검증 예시:
public record DateRangeRequest(
@Schema(description = "일정 시작일", example = "2025.09.01")
@JsonFormat(pattern = "yyyy.MM.dd")
@NotNull(message = "시작일은 필수입니다")
LocalDate startDate,
@Schema(description = "일정 종료일", example = "2025.09.03")
@JsonFormat(pattern = "yyyy.MM.dd")
@NotNull(message = "종료일은 필수입니다")
LocalDate endDate
) {
public DateRangeRequest {
if (startDate != null && endDate != null && endDate.isBefore(startDate)) {
throw new IllegalArgumentException("종료일은 시작일보다 이후여야 합니다");
}
}
}QueryDSL 레벨은 제가 제안한 방어적 체크를 그대로 유지하여 이중 안전장치를 만드는 것이 좋겠습니다.
일반적으로는 DTO 레벨에서 1차 검증하고, 데이터 접근 레벨에서는 방어적 체크를 하는 것이 클린한 아키텍처라고 생각해요! 어떻게 생각하시나요?
🏁 Script executed:
#!/bin/bash
# Let's check the PR to understand the DTO validation discussion mentioned
gh pr view 31 --repo CHA-CALL/ChaCall-Server --comments | grep -A 10 -B 5 "DTO level validation\|dto.*validation\|validation.*dto" || echo "No direct match found"
# Let's also check if there are any validation annotations already in the DateRangeRequest
cat src/main/java/konkuk/chacall/domain/foodtruck/presentation/dto/request/DateRangeRequest.java
Length of output: 586
🏁 Script executed:
#!/bin/bash
# Let's also check the FoodTruckSearchRequest to see current validation setup
cat src/main/java/konkuk/chacall/domain/foodtruck/presentation/dto/request/FoodTruckSearchRequest.java
Length of output: 2402
@buzz0331 확인해보니 현재 DTO 레벨에 validation이 전혀 없네요!
현재 상황:
DateRangeRequest에@NotNull등의 validation 어노테이션이 없음FoodTruckSearchRequest의schedules필드에@Valid어노테이션이 없음
제안하는 해결책:
1. DTO 레벨 validation 추가:
// DateRangeRequest.java
@Schema(description = "조회 기간(시작~종료). 단일 날짜는 start==end")
public record DateRangeRequest(
@Schema(description = "일정 시작일", example = "2025.09.01")
@JsonFormat(pattern = "yyyy.MM.dd")
@NotNull(message = "시작일은 필수입니다")
LocalDate startDate,
@Schema(description = "일정 종료일", example = "2025.09.03")
@JsonFormat(pattern = "yyyy.MM.dd")
@NotNull(message = "종료일은 필수입니다")
LocalDate endDate
) {
public DateRangeRequest {
if (startDate != null && endDate != null && endDate.isBefore(startDate)) {
throw new IllegalArgumentException("종료일은 시작일보다 이후여야 합니다");
}
}
}// FoodTruckSearchRequest.java에서 schedules 필드에 @Valid 추가
@Parameter(name = "schedules", ...)
@Valid // 이 어노테이션 추가
List<DateRangeRequest> schedules,2. QueryDSL 레벨에서 방어적 체크 유지 (제가 원래 제안했던 대로)
이렇게 하면 DTO에서 1차 검증, QueryDSL에서 2차 방어가 되어 더 안전한 코드가 될 것 같아요! DTO 레벨 validation을 먼저 적용해보시는 게 어떨까요?
| // 커서 기반 페이징 | ||
| where.and(foodTruck.foodTruckId.lt(lastCursor)); | ||
|
|
There was a problem hiding this comment.
커서 null 시 NPE/전부필터 위험 — 가드 필요(블로커)
lastCursor가 null이면 lt(lastCursor)에서 NPE가 납니다. 첫 페이지는 커서 조건을 생략하거나 안전한 기본값을 사용하세요.
수정 예시:
- // 커서 기반 페이징
- where.and(foodTruck.foodTruckId.lt(lastCursor));
+ // 커서 기반 페이징
+ if (lastCursor != null) {
+ where.and(foodTruck.foodTruckId.lt(lastCursor));
+ }또는 기본 커서를 Long.MAX_VALUE 등으로 부여해 첫 페이지를 안전하게 구성하세요.
📝 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.
| // 커서 기반 페이징 | |
| where.and(foodTruck.foodTruckId.lt(lastCursor)); | |
| // 커서 기반 페이징 | |
| if (lastCursor != null) { | |
| where.and(foodTruck.foodTruckId.lt(lastCursor)); | |
| } |
There was a problem hiding this comment.
수고하셨습니다. 필터링이 엄청 복잡한 로직이라 Querydsl 도입도 괜찮겠다 생각했는데, 마침 딱 도입해주셨네요! 역시 굿굿입니다~
pr 내용에 관해서는 repository 하위에 다른 패키지에 두는 게 더 나은 것 같습니다. 다만, 현재처럼 기술 스택(querydsl)로 패키지 이름을 지으면 추후에 만약에 (그럴일이 없을 것 같지만) 다른 기술 스택으로 교체하게 될 경우 패키지 이름까지 수정하게 되어서 스택에 의존성이 생길 수도 있을 것 같아요. 따라서, custom 또는 infra, impl 같은 패키지 이름을 사용하는 것 어떨지 제안드려봅니다!!
| // 요청된 수량 기준을 만족하는 트럭의 가능한 값 집합 | ||
| public static EnumSet<AvailableQuantity> acceptableFor(AvailableQuantity req) { | ||
| if (req == null) return EnumSet.noneOf(AvailableQuantity.class); | ||
|
|
||
| return switch (req) { | ||
| case LESS_THAN_50 -> | ||
| EnumSet.of(LESS_THAN_50, LESS_THAN_100, LESS_THAN_150, MORE_THAN_200, NEED_DISCUSSION); | ||
| case LESS_THAN_100 -> | ||
| EnumSet.of(LESS_THAN_100, LESS_THAN_150, MORE_THAN_200, NEED_DISCUSSION); | ||
| case LESS_THAN_150 -> | ||
| EnumSet.of(LESS_THAN_150, MORE_THAN_200, NEED_DISCUSSION); | ||
| case MORE_THAN_200 -> | ||
| EnumSet.of(MORE_THAN_200, NEED_DISCUSSION); | ||
| case NEED_DISCUSSION -> | ||
| EnumSet.of(NEED_DISCUSSION); | ||
| }; | ||
| } |
There was a problem hiding this comment.
오호 대소비교를 하지 않고 이렇게 매핑했군요. 좋습니다! 가독성도 굿이네여
| for (DateRangeRequest dateRange : request.schedules()) { | ||
| BooleanExpression covered = JPAExpressions.selectOne() | ||
| .from(availableDate) | ||
| .where(availableDate.foodTruck.foodTruckId.eq(foodTruck.foodTruckId), | ||
| availableDate.startAt.loe(dateRange.startDate()), | ||
| availableDate.endAt.goe(dateRange.endDate())) | ||
| .exists(); | ||
| any.or(covered); | ||
| } |
There was a problem hiding this comment.
p2: 현재 dateRange 내부에서 startDate와 endDate의 날짜 유효성 검증이 안되는 것 같습니다. 사실 로직상 발생하지 않는 경우긴 하지만, 그렇게 들어오는 경우 저희 조회 로직이 꼬여버릴 수도 있어서 코래 말처럼 Querydsl 레벨이나 dto 레벨 둘중 한곳에서는 유효성 검증이 들어가야 할 것 같아요!
dto 레벨 검증은 여기에서 코래가 설명하고 있네요!
| @Schema(description = "푸드트럭 검색/필터 요청") | ||
| public record FoodTruckSearchRequest( | ||
| @Schema(description = "검색어(이름/설명 LIKE)", example = "디저트") | ||
| String keyword, | ||
|
|
||
| @Parameter( | ||
| name = "regionCodes", in = ParameterIn.QUERY, | ||
| description = "지역 코드들 (prefix 검색, 여러 개 OR). 콤마로 구분", | ||
| schema = @Schema(type = "string", example = "11680,41") | ||
| ) | ||
| List<Long> regionCodes, | ||
| @Parameter( | ||
| name = "schedules", in = ParameterIn.QUERY, | ||
| description = "운영 가능 기간들(여러 개 OR). 형식: yyyy.MM.dd~yyyy.MM.dd 를 콤마로 구분", | ||
| schema = @Schema( | ||
| type = "string", | ||
| example = "2025.09.10~2025.09.11,2025.10.11~2025.10.31") | ||
| ) | ||
| List<DateRangeRequest> schedules, | ||
| @Schema(description = "수량 기준", example = "LESS_THAN_150") | ||
| AvailableQuantity availableQuantity, | ||
| @Parameter( | ||
| name = "categories", in = ParameterIn.QUERY, | ||
| description = "음식 종류(여러 개 OR, 영어 라벨). 콤마로 구분 / 가능 값: KOREAN, CHINESE, JAPANESE, WESTERN, SNACK, CAFE_DESSERT, ETC", | ||
| schema = @Schema( | ||
| type = "string", | ||
| example = "SNACK,KOREAN", | ||
| description = "가능 값: KOREAN, CHINESE, JAPANESE, WESTERN, SNACK, CAFE_DESSERT, ETC" | ||
| ) | ||
| ) | ||
| List<MenuCategory> categories, | ||
| @Schema(description = "전기 사용", example = "REQUIRED") | ||
| NeedElectricity needElectricity, | ||
| @Schema(description = "결제 방법(ANY 를 선택하면 필터 미적용)", example = "CARD") | ||
| PaymentMethod paymentMethod, | ||
|
|
||
| @Valid | ||
| CursorPagingRequest cursorPagingRequest | ||
| ) implements HasPaging { |
There was a problem hiding this comment.
p3: 이거 Request DTO에서의 Enum 사용에 대해서 한번 논의해보고 싶은데, 사실 프론트에서는 KOREAN이나 CHINESE 또는 LESS_THAN_150 이라는 Enum이 정의되어 있지 않고 웹 뷰 그대로 한식, 중식, 50인분 미만 이라고 정의되어 있을 것 같은데, Enum을 사용할때 request에서 그냥 value 값을 받고 서비스 로직에서 정적 메서드를 통해서 Enum으로 타입 변환을 하는 것 어떨까요?
이런 식으로 Enum 사용을 통일하면 현재 유효하지 않은 Enum이 들어올 때 internal server error가 발생하는 것을 INVALID_PARAM 에러로 치환할 수 있고, 프론트에서 또 다른 상수를 매핑해두지 않아도 된다는 점에서 이점이 있을 것 같긴 합니다!
상균님이 괜찮으시다고 하신다면 저도 지금까지 제가 구현해둔 api에서 enum으로 request를 받고 있는 것을 모두 value 값을 받도록 수정하겠습니다.
There was a problem hiding this comment.
원래는 현준님이 말씀하신 것처럼 value 기반으로 ENUM 치환이 뷰와의 통일성이 있을 것 같아서 고민하긴 했는데, 해당 구현을 위한 리소스가 어느정도 필요할지 감이 안와서 일단 미뤄둔 상태긴 했습니다!
그렇 value 를 받는 방식으로 일단 가보죵
| if (source == null || !source.contains("~")) | ||
| throw new IllegalArgumentException("날짜는 반드시 'yyyy.MM.dd~yyyy.MM.dd' 형식이어야합니다."); |
There was a problem hiding this comment.
p3: INVALID_DATE_INPUT 에러코드를 INVALID_DATE_FORMAT으로 바꾸고 에러 메시지도 "잘못된 날짜 형식입니다." 바꾼 후 위 분기에서 INVALID_DATE_FORMAT을 던지는거 어떨까요?
| } | ||
|
|
||
| @Override | ||
| public Slice<FoodTruck> getFoodTrucks(FoodTruckSearchRequest request) { |
| @Getter | ||
| @RequiredArgsConstructor | ||
| public enum FoodTruckStatus { | ||
| PENDING("승인 대기"), // 아직 관리자의 승인이 끝나지 않은 상태 | ||
| ON("승인 완료 - 노출"), // 승인 완료 + 사용자에게 보여지는 상태 | ||
| OFF("승인 완료 - 비노출"); // 승인 완료 + 사용자에게 보이지 않는 상태 | ||
|
|
||
| private final String description; | ||
| } No newline at end of file |
There was a problem hiding this comment.
승인 거부 같은 status는 필요 없을까요??
There was a problem hiding this comment.
이 부분은 승인 거부(반려)시에 어떤 식으로 처리할 지를 PM 님과 좀 더 이야기를 나누어봐야할 것 같긴 합니다.
그래도 아마 승인 거부 상태를 만드는 게 좋을 것 같긴 하네요! 만들어둘게용
infra 좋은 것 같습니다! 해당 방향으로 패키지명 변경해볼게유 |
302cc0a to
86cbd93
Compare
There was a problem hiding this comment.
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 (4)
src/main/java/konkuk/chacall/domain/reservation/application/status/ReservationStatusService.java (1)
13-18: 상태 변경 메서드에 @transactional 누락 — 더티 체킹 반영 안 될 수 있음엔티티 필드 변경이 트랜잭션 밖에서 일어나면 DB 반영이 보장되지 않습니다. 클래스 레벨 readOnly 기본값과 변경 메서드에 한정된 write 트랜잭션을 권장합니다.
package konkuk.chacall.domain.reservation.application.status; import konkuk.chacall.domain.reservation.domain.model.Reservation; import konkuk.chacall.domain.reservation.domain.repository.ReservationRepository; import konkuk.chacall.domain.reservation.presentation.dto.request.UpdateReservationStatusRequest; import konkuk.chacall.domain.reservation.presentation.dto.response.ReservationStatusResponse; import konkuk.chacall.domain.user.domain.model.User; import konkuk.chacall.global.common.exception.EntityNotFoundException; import konkuk.chacall.global.common.exception.code.ErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class ReservationStatusService { private final ReservationRepository reservationRepository; - public ReservationStatusResponse updateReservationStatusToConfirmedRequested(Long reservationId, UpdateReservationStatusRequest request, User member) { + @Transactional + public ReservationStatusResponse updateReservationStatusToConfirmedRequested(Long reservationId, UpdateReservationStatusRequest request, User member) { - public ReservationStatusResponse updateReservationStatusToConfirmed(Long reservationId, UpdateReservationStatusRequest request, User owner) { + @Transactional + public ReservationStatusResponse updateReservationStatusToConfirmed(Long reservationId, UpdateReservationStatusRequest request, User owner) { - public ReservationStatusResponse updateReservationStatusToCancelled(Long reservationId, UpdateReservationStatusRequest request, User user) { + @Transactional + public ReservationStatusResponse updateReservationStatusToCancelled(Long reservationId, UpdateReservationStatusRequest request, User user) {Also applies to: 19-28, 30-39, 41-50
src/main/java/konkuk/chacall/global/common/converter/ReservationDateListConverter.java (1)
42-48: 파싱 예외(DateTimeParseException) 누락 처리 — 400으로 매핑 필요현재 포맷 불일치 시 런타임 예외가 그대로 전파될 수 있습니다. 도메인 예외로 감싸 4xx로 응답되도록 처리하세요.
String[] dates = trimmedPart.split(RANGE_DELIM); if (dates.length != 2) { throw new DomainRuleException(ErrorCode.INVALID_DATE_FORMAT); } - LocalDate start = LocalDate.parse(dates[0].trim(), DOT); // ISO yyyy-MM-dd - LocalDate end = LocalDate.parse(dates[1].trim(), DOT); - list.add(new DateRange(start, end)); + try { + LocalDate start = LocalDate.parse(dates[0].trim(), DOT); + LocalDate end = LocalDate.parse(dates[1].trim(), DOT); + list.add(new DateRange(start, end)); + } catch (Exception e) { + throw new DomainRuleException(ErrorCode.INVALID_DATE_FORMAT); + }src/main/java/konkuk/chacall/domain/reservation/presentation/dto/request/UpdateReservationStatusRequest.java (1)
7-15: RequestBody 한글 enum 역직렬화 실패 — ReservationStatus에 @JsonCreator 또는 ObjectMapper 설정 필요ReservationStatus에 @jsonvalue만 있고 @JsonCreator가 보이지 않으며 JsonConfig는 READ_ENUMS_USING_TO_STRING 등 관련 설정이 없습니다(직렬화는 한글로 나오나 RequestBody로 "예약 대기" 같은 한글을 보내면 역직렬화 오류 발생). EnumValueConverter는 쿼리/경로변수용이므로 RequestBody에는 영향 없음.
조치(간단히):
- src/main/java/konkuk/chacall/domain/reservation/domain/value/ReservationStatus.java에 static fromValue(String) + @JsonCreator 추가
- 또는 src/main/java/konkuk/chacall/global/config/JsonConfig.java에서 ObjectMapper에 DeserializationFeature.READ_ENUMS_USING_TO_STRING 활성화하거나 커스텀 디시리얼라이저 등록
- 참고: src/main/java/konkuk/chacall/global/common/converter/EnumValueConverter.java (쿼리/경로변수 전용)
src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java (1)
22-22: API_INVALID_TYPE과 INVALID_ENUM_VALUE 코드 중복API_INVALID_TYPE과 INVALID_ENUM_VALUE이 같은 코드(40003)를 사용하고 있습니다. 이는 로깅이나 모니터링 시 혼란을 야기할 수 있습니다.
- INVALID_ENUM_VALUE(HttpStatus.BAD_REQUEST, 40003, "허용되지 않는 enum 값입니다."), + INVALID_ENUM_VALUE(HttpStatus.BAD_REQUEST, 40004, "허용되지 않는 enum 값입니다."),
🧹 Nitpick comments (18)
src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/MenuRepository.java (1)
11-13: @Modifying 벌크 삭제 시 1차 캐시 정합성과 트랜잭션 보장 확인
- 제안: clear/flush 자동화로 영속성 컨텍스트 불일치 방지.
- 검증: FoodTruck 식별자 필드명이 foodTruckId가 맞는지(id 사용 여부) 확인.
- 주의: 벌크 삭제는 엔티티 이벤트(@PreRemove 등)와 연관 관계 cascade를 트리거하지 않습니다.
적용 가능한 개선안:
- @Modifying + @Modifying(clearAutomatically = true, flushAutomatically = true) @Query("DELETE FROM Menu m WHERE m.foodTruck.foodTruckId = :foodTruckId") void deleteAllByFoodTruckId(@Param("foodTruckId") Long foodTruckId);만약 FoodTruck의 식별자 필드가 id 라면 아래로 교체를 고려하세요:
- @Query("DELETE FROM Menu m WHERE m.foodTruck.foodTruckId = :foodTruckId") + @Query("DELETE FROM Menu m WHERE m.foodTruck.id = :foodTruckId")서비스 레이어에서 @transactional 경계 내에서 호출되는지 확인 부탁드립니다.
src/main/java/konkuk/chacall/domain/member/presentation/dto/response/SavedFoodTruckResponse.java (2)
20-28: DTO 매핑 시 NPE 가능성 점검 (사진/평점 정보 null 케이스)
- photoUrl: getFoodTruckPhotoList() 또는 getMainPhotoUrl()가 null일 수 있나요?
- ratingInfo: null일 경우 NPE 발생. 도메인 불변식으로 항상 존재한다면 OK, 아니라면 가드 로직/기본값 필요.
아래처럼 null‑세이프 매핑을 권장합니다(정책에 맞게 기본값은 조정).
public static SavedFoodTruckResponse of(FoodTruck foodTruck) { - return new SavedFoodTruckResponse( - foodTruck.getFoodTruckId(), - foodTruck.getName(), - foodTruck.getFoodTruckPhotoList().getMainPhotoUrl(), // 대표 사진 (첫 번째 사진) - foodTruck.getDescription(), - foodTruck.getRatingInfo().getAverageRating(), - foodTruck.getRatingInfo().getRatingCount() - ); + String photoUrl = + foodTruck.getFoodTruckPhotoList() != null + ? foodTruck.getFoodTruckPhotoList().getMainPhotoUrl() + : null; + var ratingInfo = foodTruck.getRatingInfo(); + Double averageRating = ratingInfo != null ? ratingInfo.getAverageRating() : null; // 혹은 0.0 + Integer ratingCount = ratingInfo != null ? ratingInfo.getRatingCount() : null; // 혹은 0 + + return new SavedFoodTruckResponse( + foodTruck.getFoodTruckId(), + foodTruck.getName(), + photoUrl, + foodTruck.getDescription(), + averageRating, + ratingCount + ); }추가로, 응답에서 null 필드를 숨기려면 글로벌 ObjectMapper에 JsonInclude.NON_NULL을 설정했는지도 확인 부탁드립니다.
24-24: 주석 문구 정확성 정리메서드명이 ‘Main’ 사진을 반환한다면 “첫 번째 사진”이라는 주석은 오해 소지가 있습니다. 도메인 규칙에 맞춰 문구를 “대표 사진 URL” 정도로 단순화하거나 제거를 권장합니다.
- foodTruck.getFoodTruckPhotoList().getMainPhotoUrl(), // 대표 사진 (첫 번째 사진) + foodTruck.getFoodTruckPhotoList().getMainPhotoUrl(),src/main/java/konkuk/chacall/domain/reservation/application/status/ReservationStatusService.java (1)
27-27: 응답에 사람친화 문자열만 노출 — code(기계가독) + label(표시문구) 병행 제공 권장클라이언트 안정성을 위해 enum name(또는 코드)와 표시문구를 함께 반환하는 것을 권장합니다. 비호환 변경 없이도 확장 가능합니다.
- return new ReservationStatusResponse(reservation.getReservationStatus().getValue()); + return new ReservationStatusResponse( + reservation.getReservationStatus().name(), + reservation.getReservationStatus().getValue() + );ReservationStatusResponse 변경 예:
package konkuk.chacall.domain.reservation.presentation.dto.response; import io.swagger.v3.oas.annotations.media.Schema; -public record ReservationStatusResponse( - @Schema(description = "예약 상태", example = "예약 확정") - String reservationStatus -) { -} +public record ReservationStatusResponse( + @Schema(description = "예약 상태 코드", example = "CONFIRMED") + String reservationStatusCode, + @Schema(description = "예약 상태 라벨", example = "예약 확정") + String reservationStatusLabel +) {}Also applies to: 38-38, 49-49, 58-58
build.gradle (1)
61-65: QueryDSL 의존성 추가 LGTM. IDE에서 Q-타입 인식 보완 제안생성된 Q 클래스가 IDE에서 바로 인식되지 않는 경우가 있습니다. 아래 설정을 추가하면 안정적입니다.
dependencies { // ... implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8' // QueryDSL implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" annotationProcessor "jakarta.persistence:jakarta.persistence-api:3.1.0" annotationProcessor "jakarta.annotation:jakarta.annotation-api:2.1.1" } +// IDE가 생성 소스를 소스 루트로 인식하도록 보조 +sourceSets { + main { + java { + srcDirs += 'build/generated/sources/annotationProcessor/java/main' + } + } +}src/main/java/konkuk/chacall/global/common/dto/EnumValue.java (1)
3-5: 공통 EnumValue 도입 좋습니다단순하고 재사용성 높습니다. 패키지 위치는 dto 보다는 common.value(또는 types) 쪽이 약간 더 자연스러울 수 있습니다.
src/main/java/konkuk/chacall/domain/reservation/presentation/dto/response/ReservationStatusResponse.java (1)
6-8: 문자열 단일 필드 응답은 변경 민감 — code+label 병행 제공 권장표시문구(i18n) 변경 시 클라이언트가 깨질 수 있습니다. 코드와 라벨을 모두 제공하는 구조를 권장합니다(서비스 변경 예시는 앞 코멘트 참고).
src/main/java/konkuk/chacall/global/common/converter/ReservationDateListConverter.java (1)
15-17: 주석 포맷 표기와 실제 포맷(DOT) 불일치주석은 yyyy-MM-dd로, 코드는 yyyy.MM.dd로 파싱합니다. 혼선을 막기 위해 주석 정정 권장.
- * DB: "yyyy-MM-dd,yyyy-MM-dd,..." <-> 도메인: ReservationDateList + * DB: "yyyy.MM.dd~yyyy.MM.dd,yyyy.MM.dd~yyyy.MM.dd" <-> 도메인: ReservationDateList @@ - private static final DateTimeFormatter DOT = DateTimeFormatter.ofPattern("yyyy.MM.dd"); + private static final DateTimeFormatter DOT = DateTimeFormatter.ofPattern("yyyy.MM.dd"); // DB 포맷Also applies to: 23-24
src/main/java/konkuk/chacall/domain/foodtruck/domain/value/MenuCategory.java (1)
20-27: from(String) 선형 탐색 → 맵 캐시로 상수시간 변환 권장호출 빈도가 높다면 맵 캐시로 단순화/가속 가능합니다.
@Getter @RequiredArgsConstructor public enum MenuCategory implements EnumValue { @@ private final String value; + private static final java.util.Map<String, MenuCategory> BY_VALUE; + static { + java.util.Map<String, MenuCategory> m = new java.util.HashMap<>(); + for (MenuCategory c : values()) m.put(c.getValue(), c); + BY_VALUE = java.util.Collections.unmodifiableMap(m); + } + public static MenuCategory from(String value) { - for (MenuCategory category : MenuCategory.values()) { - if (category.getValue().equals(value)) { - return category; - } - } - throw new IllegalArgumentException("Unknown MenuCategory value: " + value); + MenuCategory c = BY_VALUE.get(value); + if (c == null) throw new IllegalArgumentException("Unknown MenuCategory value: " + value); + return c; }src/main/java/konkuk/chacall/domain/reservation/domain/value/ReservationDateList.java (2)
49-52: 날짜 범위 유효성(시작 > 종료) 검증 추가 제안.start > end인 입력을 INVALID_DATE_RANGE로 막아주세요.
LocalDate start = parseDot(dates[0].trim()); LocalDate end = parseDot(dates[1].trim()); + if (end.isBefore(start)) { + throw new DomainRuleException(ErrorCode.INVALID_DATE_RANGE); + } list.add(new DateRange(start, end));
27-29: 불필요한 null 체크 제거 가능.ranges는 final이고 of()에서 null → List.of()로 정규화됩니다. 간결화 권장.
- public boolean isEmpty() { - return ranges == null || ranges.isEmpty(); - } + public boolean isEmpty() { + return ranges.isEmpty(); + }src/main/java/konkuk/chacall/global/common/exception/handler/GlobalExceptionHandler.java (1)
131-146: Enum 역직렬화 실패 시, 필드 경로와 허용값까지 응답에 포함 권장.현재는 “잘못된 값”만 제공됩니다. 필드명과 허용값(한글 value 또는 enum name)을 함께 주면 DX가 크게 개선됩니다.
- if (cause instanceof InvalidFormatException ife && ife.getTargetType().isEnum()) { - return ResponseEntity - .status(ErrorCode.INVALID_ENUM_VALUE.getHttpStatus()) - .body(ErrorResponse.of(ErrorCode.INVALID_ENUM_VALUE, - "잘못된 값: " + ife.getValue())); - } + if (cause instanceof InvalidFormatException ife && ife.getTargetType().isEnum()) { + Class<?> target = ife.getTargetType(); + String field = ife.getPath() == null ? "" : + ife.getPath().stream() + .map(ref -> ref.getFieldName()) + .filter(Objects::nonNull) + .collect(Collectors.joining(".")); + String allowed = java.util.stream.Stream.of(target.getEnumConstants()) + .map(constant -> { + if (constant instanceof konkuk.chacall.global.common.dto.EnumValue ev) return ev.getValue(); + return ((Enum<?>) constant).name(); + }) + .collect(Collectors.joining(", ")); + String msg = String.format("%s의 잘못된 값: %s (허용값: %s)", + field.isBlank() ? "필드" : field, String.valueOf(ife.getValue()), allowed); + return ResponseEntity + .status(ErrorCode.INVALID_ENUM_VALUE.getHttpStatus()) + .body(ErrorResponse.of(ErrorCode.INVALID_ENUM_VALUE, msg)); + }필요 import:
- java.util.stream.Stream (완전수식 사용했으므로 생략 가능)
- com.fasterxml.jackson.databind.JsonMappingException (ife.getPath 참조 시)
- konkuk.chacall.global.common.dto.EnumValue (형 체크)
src/main/java/konkuk/chacall/domain/member/presentation/dto/request/GetReservationHistoryRequest.java (1)
30-33: 레코드 파라미터 쉼표 위치 가독성 개선.쉼표를 다음 줄 단독 배치 대신 동일 라인 끝에 두는 것이 일반적입니다.
- @NotNull(message = "예약 상태는 필수입니다.") - ReservationStatus status - , + @NotNull(message = "예약 상태는 필수입니다.") + ReservationStatus status, @Valid CursorPagingRequest cursorPagingRequestsrc/main/java/konkuk/chacall/domain/foodtruck/domain/value/FoodTruckStatus.java (1)
8-15: EnumValue 일원화 적용 고려.외부 노출/필터에 사용될 가능성이 있으면 다른 enum과 동일하게 EnumValue 적용 및 값 변환 일관성 확보를 권장합니다.
src/main/java/konkuk/chacall/global/common/converter/DateRangeRequestConverter.java (1)
24-26: 빈 문자열 처리 검증 필요
split("~", 2)후 각 부분이 비어있는 경우를 추가로 검증하면 더 견고한 코드가 됩니다. 예를 들어 "2024.01.01" 또는 "2024.01.01" 같은 입력의 경우 현재는 DateTimeParseException으로 처리되지만, 명시적인 검증이 있으면 더 명확합니다.try { String[] parts = source.split("~", 2); + if (parts[0].trim().isEmpty() || parts[1].trim().isEmpty()) { + throw new DomainRuleException(ErrorCode.INVALID_DATE_FORMAT); + } LocalDate startDate = LocalDate.parse(parts[0].trim(), FORMATTER); LocalDate endDate = LocalDate.parse(parts[1].trim(), FORMATTER);src/main/java/konkuk/chacall/global/common/converter/EnumValueConverter.java (1)
32-35: enum 값 대소문자 처리 고려현재는 정확히 일치하는 값만 매칭됩니다. API 사용성 향상을 위해 대소문자 무시 옵션을 고려해볼 수 있습니다.
대소문자 구분이 필요한지 확인이 필요합니다. 만약 유연한 처리가 필요하다면:
return Arrays.stream(targetType.getEnumConstants()) - .filter(e -> e.getValue().equals(trimmedSource)) + .filter(e -> e.getValue().equalsIgnoreCase(trimmedSource)) .findFirst()src/main/java/konkuk/chacall/domain/foodtruck/presentation/dto/request/FoodTruckSearchRequest.java (1)
22-27: 배열 파라미터 문서화 개선(선택)
regionCodes가 CSV로 동작한다면 OAS에서는 array 스키마/스타일(form, explode=false)로 명시하면 Swagger UI 사용성이 좋아집니다. 현재 string 예시는 동작엔 문제 없으나 문서 가독성이 떨어집니다.src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/infra/FoodTruckSearchRepositoryImpl.java (1)
47-65: 지역 prefix 필터 성능 주의 — stringValue().startsWith는 인덱스 무력화 가능숫자 코드에 대해
region.regionCode.stringValue().startsWith(...)는 캐스팅+LIKE로 인덱스를 타지 않을 수 있습니다. 트래픽/데이터 증가 시 병목이 될 수 있으니, 가능하면 숫자 범위 조건(프리픽스에 해당하는 min/max 계산)으로 전환을 고려하세요.예: prefix 116이면 [116000, 116999] 범위(between)로 변환. 코드 길이가 가변이면 길이별 범위 사전 계산 또는 정규화 컬럼 추가를 고려.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (51)
build.gradle(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/application/FoodTruckService.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/application/search/FoodTruckSearchService.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/model/AvailableDate.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/model/FoodTruck.java(2 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/model/FoodTruckServiceArea.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/model/Menu.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/AvailableDateRepository.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/FoodTruckRepository.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/FoodTruckServiceAreaRepository.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/MenuRepository.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/infra/FoodTruckSearchRepository.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/infra/FoodTruckSearchRepositoryImpl.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/value/AvailableQuantity.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/value/FoodTruckStatus.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/value/MenuCategory.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/value/NeedElectricity.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/value/PaymentMethod.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/presentation/FoodTruckController.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/presentation/dto/request/DateRangeRequest.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/presentation/dto/request/FoodTruckSearchRequest.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/presentation/dto/response/FoodTruckResponse.java(1 hunks)src/main/java/konkuk/chacall/domain/member/application/foodtruck/SavedFoodTruckService.java(1 hunks)src/main/java/konkuk/chacall/domain/member/application/rating/RatingService.java(1 hunks)src/main/java/konkuk/chacall/domain/member/domain/Rating.java(1 hunks)src/main/java/konkuk/chacall/domain/member/domain/SavedFoodTruck.java(1 hunks)src/main/java/konkuk/chacall/domain/member/domain/repository/RatingRepository.java(1 hunks)src/main/java/konkuk/chacall/domain/member/domain/repository/SavedFoodTruckRepository.java(1 hunks)src/main/java/konkuk/chacall/domain/member/presentation/dto/request/GetReservationHistoryRequest.java(2 hunks)src/main/java/konkuk/chacall/domain/member/presentation/dto/response/MemberReservationDetailResponse.java(1 hunks)src/main/java/konkuk/chacall/domain/member/presentation/dto/response/MemberReservationHistoryResponse.java(1 hunks)src/main/java/konkuk/chacall/domain/member/presentation/dto/response/ReservationForRatingResponse.java(1 hunks)src/main/java/konkuk/chacall/domain/member/presentation/dto/response/SavedFoodTruckResponse.java(1 hunks)src/main/java/konkuk/chacall/domain/owner/application/myfoodtruck/MyFoodTruckService.java(1 hunks)src/main/java/konkuk/chacall/domain/owner/presentation/dto/request/GetReservationHistoryRequest.java(2 hunks)src/main/java/konkuk/chacall/domain/owner/presentation/dto/response/MyFoodTruckResponse.java(1 hunks)src/main/java/konkuk/chacall/domain/reservation/application/info/ReservationInfoService.java(1 hunks)src/main/java/konkuk/chacall/domain/reservation/application/status/ReservationStatusService.java(4 hunks)src/main/java/konkuk/chacall/domain/reservation/domain/model/Reservation.java(1 hunks)src/main/java/konkuk/chacall/domain/reservation/domain/value/ReservationDateList.java(2 hunks)src/main/java/konkuk/chacall/domain/reservation/domain/value/ReservationStatus.java(3 hunks)src/main/java/konkuk/chacall/domain/reservation/presentation/dto/request/UpdateReservationStatusRequest.java(1 hunks)src/main/java/konkuk/chacall/domain/reservation/presentation/dto/response/ReservationStatusResponse.java(1 hunks)src/main/java/konkuk/chacall/global/common/converter/DateRangeRequestConverter.java(1 hunks)src/main/java/konkuk/chacall/global/common/converter/EnumValueConverter.java(1 hunks)src/main/java/konkuk/chacall/global/common/converter/ReservationDateListConverter.java(1 hunks)src/main/java/konkuk/chacall/global/common/dto/EnumValue.java(1 hunks)src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java(1 hunks)src/main/java/konkuk/chacall/global/common/exception/handler/GlobalExceptionHandler.java(3 hunks)src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java(2 hunks)src/main/java/konkuk/chacall/global/config/QuerydslConfig.java(1 hunks)
✅ Files skipped from review due to trivial changes (3)
- src/main/java/konkuk/chacall/domain/reservation/application/info/ReservationInfoService.java
- src/main/java/konkuk/chacall/domain/reservation/domain/model/Reservation.java
- src/main/java/konkuk/chacall/domain/foodtruck/domain/model/AvailableDate.java
🚧 Files skipped from review as they are similar to previous changes (21)
- src/main/java/konkuk/chacall/domain/member/domain/Rating.java
- src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/AvailableDateRepository.java
- src/main/java/konkuk/chacall/domain/foodtruck/presentation/dto/request/DateRangeRequest.java
- src/main/java/konkuk/chacall/domain/member/domain/SavedFoodTruck.java
- src/main/java/konkuk/chacall/domain/member/presentation/dto/response/ReservationForRatingResponse.java
- src/main/java/konkuk/chacall/domain/foodtruck/domain/model/FoodTruckServiceArea.java
- src/main/java/konkuk/chacall/domain/foodtruck/application/FoodTruckService.java
- src/main/java/konkuk/chacall/domain/foodtruck/application/search/FoodTruckSearchService.java
- src/main/java/konkuk/chacall/domain/member/application/foodtruck/SavedFoodTruckService.java
- src/main/java/konkuk/chacall/domain/member/application/rating/RatingService.java
- src/main/java/konkuk/chacall/domain/member/presentation/dto/response/MemberReservationHistoryResponse.java
- src/main/java/konkuk/chacall/domain/member/presentation/dto/response/MemberReservationDetailResponse.java
- src/main/java/konkuk/chacall/domain/foodtruck/presentation/dto/response/FoodTruckResponse.java
- src/main/java/konkuk/chacall/global/config/QuerydslConfig.java
- src/main/java/konkuk/chacall/domain/owner/presentation/dto/response/MyFoodTruckResponse.java
- src/main/java/konkuk/chacall/domain/owner/application/myfoodtruck/MyFoodTruckService.java
- src/main/java/konkuk/chacall/domain/foodtruck/domain/model/FoodTruck.java
- src/main/java/konkuk/chacall/domain/member/domain/repository/SavedFoodTruckRepository.java
- src/main/java/konkuk/chacall/domain/foodtruck/domain/model/Menu.java
- src/main/java/konkuk/chacall/domain/member/domain/repository/RatingRepository.java
- src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/FoodTruckServiceAreaRepository.java
🧰 Additional context used
🧬 Code graph analysis (3)
src/main/java/konkuk/chacall/domain/foodtruck/domain/value/MenuCategory.java (1)
src/main/java/konkuk/chacall/domain/foodtruck/domain/value/MenuCategoryList.java (1)
RequiredArgsConstructor(9-29)
src/main/java/konkuk/chacall/domain/foodtruck/presentation/FoodTruckController.java (1)
src/main/java/konkuk/chacall/domain/foodtruck/application/FoodTruckService.java (1)
RequiredArgsConstructor(11-21)
src/main/java/konkuk/chacall/global/common/converter/DateRangeRequestConverter.java (1)
src/main/java/konkuk/chacall/global/common/converter/ReservationDateListConverter.java (1)
Converter(18-52)
🔇 Additional comments (22)
src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/MenuRepository.java (1)
3-3: 모델 패키지 경로 변경 LGTM리팩터링 방향과 일치하며 리포지토리/JPQL 동작에 영향 없습니다.
src/main/java/konkuk/chacall/domain/member/presentation/dto/response/SavedFoodTruckResponse.java (1)
4-4: 패키지 경로 변경 확인 — 레거시 import 없음레거시 경로(konkuk.chacall.domain.foodtruck.domain.FoodTruck)가 전역 검색에서 발견되지 않았고, 모든 참조가 konkuk.chacall.domain.foodtruck.domain.model.FoodTruck로 업데이트되어 있습니다.
src/main/java/konkuk/chacall/domain/foodtruck/domain/value/MenuCategory.java (1)
9-9: EnumValue 구현 LGTM검색/변환 일관성에 도움이 됩니다.
src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java (1)
132-133: 스웨거 에러 코드 매핑 업데이트 적절 — ErrorCode 상수 확인됨
INVALID_DATE_FORMAT과 INVALID_ENUM_VALUE가 ErrorCode(enum)에 정의되어 있어 반영에 문제 없습니다.src/main/java/konkuk/chacall/domain/foodtruck/domain/value/PaymentMethod.java (1)
9-10: 하위호환성 경고 — PaymentMethod enum 변경으로 DB 역직렬화 실패 가능PaymentMethod는 @Enumerated(EnumType.STRING)로 저장(src/main/java/konkuk/chacall/domain/foodtruck/domain/model/FoodTruck.java). 코드상에는 '입금'/'DEPOSIT' 문자열이 없고 현재는 BANK_TRANSFER("계좌이체")만 존재하지만, 운영 DB에 과거 값이 남아 있으면 JPA 조회 시 enum 매핑(역직렬화) 실패가 발생합니다.
조치(택1):
- 데이터 마이그레이션: DB의 기존 값(예: 'DEPOSIT'/'입금') → 'BANK_TRANSFER'
- 또는 컨버터/별칭 허용: PaymentMethod.fromValueOrAlias(...) 또는 AttributeConverter로 과거 값 매핑 허용
- 또는 API 문서/예시 전면 교체
DB에 과거 enum 값이 남아 있는지 확인하세요 (예: SELECT DISTINCT payment_method FROM food_trucks).
src/main/java/konkuk/chacall/domain/reservation/domain/value/ReservationDateList.java (2)
74-80: 파싱 예외를 INVALID_DATE_FORMAT으로 매핑한 점 👍의도에 맞고 일관적입니다.
45-47: 에러코드 변경 적절 — INVALID_DATE_FORMAT으로 일관화 확인
SwaggerResponseDescription, ErrorCode, ReservationDateList, DateRangeRequestConverter, ReservationDateListConverter 등에서 INVALID_DATE_FORMAT을 사용 중이며 INVALID_DATE_INPUT은 검색 결과에 없습니다.src/main/java/konkuk/chacall/domain/foodtruck/domain/value/FoodTruckStatus.java (1)
9-13: REJECTED 상태 추가 반영 좋습니다.이전 코멘트에서 제기된 “승인 거부” 요구사항을 충족합니다.
src/main/java/konkuk/chacall/domain/foodtruck/presentation/FoodTruckController.java (1)
35-39: DTO 필드별 Bean Validation 보강 필요(이전 코멘트와 동일).@Valid는 있으나 FoodTruckSearchRequest 내부 필드 제약이 부족하면 유효성 검사가 빈약합니다. size 상한, lastCursor 범위, enum 허용값, 날짜 포맷 등 제약 추가를 권장합니다.
#!/bin/bash # FoodTruckSearchRequest의 제약 어노테이션 유무 점검 fd -uu --type f 'FoodTruckSearchRequest.java' rg -n -C2 -S 'record\s+FoodTruckSearchRequest|class\s+FoodTruckSearchRequest' $(fd -uu 'FoodTruckSearchRequest.java') rg -n -C2 -S '@(NotNull|NotBlank|Positive|PositiveOrZero|Size|Max|Min|Pattern|Past|Future|Valid)' $(fd -uu 'FoodTruckSearchRequest.java')src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/infra/FoodTruckSearchRepository.java (1)
1-9: 확인 완료 — 커스텀 리포지토리 연결 정상FoodTruckRepository가 FoodTruckSearchRepository를 확장(src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/FoodTruckRepository.java)하고, 구현체 FoodTruckSearchRepositoryImpl이 존재하며 @repository로 등록되어 있습니다.
src/main/java/konkuk/chacall/global/common/exception/handler/GlobalExceptionHandler.java (1)
136-136: 확인: Java 17로 설정되어 있어 instanceof 패턴 매칭 사용 가능.
build.gradle의 java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }로 설정되어 있습니다.src/main/java/konkuk/chacall/domain/member/presentation/dto/request/GetReservationHistoryRequest.java (1)
13-28: 확인: EnumValueConverter(@component) 등록되어 있으며 한글 쿼리값 매핑이 정상 동작합니다.ReservationStatus#getValue()가 예시 한글 문자열(예: "예약 대기")과 일치하고 EnumValueConverter가 빈으로 등록되어 있어 Spring Boot가 ConversionService에 자동으로 등록하므로 status=예약 대기 → ReservationStatus로 정상 변환됩니다. 참고: src/main/java/konkuk/chacall/global/common/converter/EnumValueConverter.java, src/main/java/konkuk/chacall/domain/reservation/domain/value/ReservationStatus.java, src/main/java/konkuk/chacall/global/config/WebConfig.java
src/main/java/konkuk/chacall/domain/reservation/domain/value/ReservationStatus.java (1)
3-4: EnumValue 인터페이스 구현 확인 완료ReservationStatus enum이 EnumValue 인터페이스를 구현하고 @jsonvalue 어노테이션을 적절히 적용한 것을 확인했습니다. 이를 통해 JSON 직렬화/역직렬화 시 일관된 문자열 값 처리가 가능해집니다.
Also applies to: 14-14, 23-24
src/main/java/konkuk/chacall/global/common/converter/DateRangeRequestConverter.java (1)
20-35: 날짜 형식 검증 로직이 적절히 구현되었습니다이전 리뷰에서 제안한 대로 날짜 파싱 예외 처리와 날짜 범위 유효성 검증이 잘 구현되었습니다:
- DateTimeParseException을 catch하여 INVALID_DATE_FORMAT 에러 던지기
- 종료일이 시작일보다 이전인 경우 INVALID_DATE_RANGE 에러 던지기
src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/FoodTruckRepository.java (1)
3-4: QueryDSL 검색 기능 통합 확인FoodTruckSearchRepository 인터페이스를 확장하여 QueryDSL 기반 검색 기능을 통합한 것이 적절합니다. 인프라 패키지로의 분리도 잘 되어있습니다.
Also applies to: 11-11
src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java (1)
15-17: 날짜 관련 에러코드 통합 완료INVALID_DATE_FORMAT과 INVALID_DATE_RANGE 에러코드를 Reservation 섹션에서 공통 영역으로 이동한 것이 적절합니다. 이제 여러 도메인에서 일관되게 사용할 수 있습니다.
src/main/java/konkuk/chacall/domain/foodtruck/presentation/dto/request/FoodTruckSearchRequest.java (2)
29-35: 스케줄 DTO에 대한 @Valid 누락 — 하위 검증 전파 필요
schedules컬렉션 원소(DateRangeRequest)의 Bean Validation이 전파되지 않습니다. @Valid 추가를 권장합니다.적용 예시:
@Parameter( name = "schedules", in = ParameterIn.QUERY, description = "운영 가능 기간들(여러 개 OR). 형식: yyyy.MM.dd~yyyy.MM.dd 를 콤마로 구분", schema = @Schema(type = "string", example = "2025.09.10~2025.09.11,2025.10.11~2025.10.31") ) + @Valid List<DateRangeRequest> schedules,
51-80: 전역 Enum 변환기(EnumValueConverter) 존재 — 바인딩 문제 없음src/main/java/konkuk/chacall/global/common/converter/EnumValueConverter.java가 @component로 구현되어 있고 대상 Enum들(AvailableQuantity, MenuCategory, NeedElectricity, PaymentMethod)이 global.common.dto.EnumValue를 구현하므로 한글 값으로 들어오는 쿼리 파라미터는 전역 Converter로 정상 매핑됩니다.
src/main/java/konkuk/chacall/domain/foodtruck/domain/value/AvailableQuantity.java (1)
23-39: 수량 필터 매핑 로직 적절 — 요구사항과 부합합니다요청 임계값 이하 트럭 포함(상위 용량 트럭 허용) 규칙이
acceptableFor에 잘 반영되어 있습니다.src/main/java/konkuk/chacall/domain/foodtruck/domain/repository/infra/FoodTruckSearchRepositoryImpl.java (2)
71-79: 스케줄 필터: null/역전 기간 가드 없음 → NPE 위험(블로커)
dateRange또는 start/end가 null이거나 역전된 경우 NPE/잘못된 조건이 생성됩니다. 방어 코드 추가 필요.적용 예시:
- for (DateRangeRequest dateRange : request.schedules()) { - BooleanExpression covered = JPAExpressions.selectOne() + for (DateRangeRequest dateRange : request.schedules()) { + if (dateRange == null || dateRange.startDate() == null || dateRange.endDate() == null) { + continue; + } + if (dateRange.endDate().isBefore(dateRange.startDate())) { + continue; + } + BooleanExpression covered = JPAExpressions.selectOne() .from(availableDate) .where(availableDate.foodTruck.foodTruckId.eq(foodTruck.foodTruckId), availableDate.startAt.loe(dateRange.startDate()), availableDate.endAt.goe(dateRange.endDate())) .exists(); any.or(covered); }
122-124: 커서 페이징: lastCursor null 시 NPE(블로커)첫 페이지에서
paging.cursor()가 null일 수 있습니다. null 가드로 조건을 추가하세요.적용 예시:
- // 커서 기반 페이징 - where.and(foodTruck.foodTruckId.lt(lastCursor)); + // 커서 기반 페이징 + if (lastCursor != null) { + where.and(foodTruck.foodTruckId.lt(lastCursor)); + }src/main/java/konkuk/chacall/domain/owner/presentation/dto/request/GetReservationHistoryRequest.java (1)
13-31: Enum 값(value) 기반 바인딩 — 처리 완료
EnumValueConverter(ConverterFactory<String, EnumValue>)가 @component로 등록되어 있고 ReservationStatus가 EnumValue를 구현하므로 Spring ConversionService가 한글 value → ReservationStatus 바인딩을 처리합니다. (참고: src/main/java/konkuk/chacall/global/common/converter/EnumValueConverter.java, src/main/java/konkuk/chacall/domain/reservation/domain/value/ReservationStatus.java)
| public enum NeedElectricity implements EnumValue { | ||
| REQUIRED("가능"), | ||
| NOT_REQUIRED("불가능"), | ||
| NEED_DISCUSSION("논의 필요"); |
There was a problem hiding this comment.
🧹 Nitpick
도메인 의미와 표시값의 불일치 가능성.
NeedElectricity.REQUIRED → "가능", NOT_REQUIRED → "불가능"은 “필요 여부”와 어울리지 않습니다. “필요/불필요”로 바꾸거나, enum명을 ElectricitySupport 등으로 조정 고려해 주세요.
🤖 Prompt for AI Agents
In
src/main/java/konkuk/chacall/domain/foodtruck/domain/value/NeedElectricity.java
around lines 7 to 10, the enum name expresses "need" but the display values are
"가능/불가능" (support availability), causing semantic mismatch; either (A) rename
the enum to ElectricitySupport (or similar) to reflect availability and keep the
current display strings, or (B) change the display strings to "필요"/"불필요"/"논의 필요"
to match the current enum name; apply the chosen change consistently across
code: update enum identifier, toString/label methods, any JSON/DB mapping,
conversion utilities, and usages/tests to prevent breakage.
| public T convert(String source) { | ||
|
|
||
| String trimmedSource = source.trim(); | ||
|
|
||
| return Arrays.stream(targetType.getEnumConstants()) | ||
| .filter(e -> e.getValue().equals(trimmedSource)) | ||
| .findFirst() | ||
| .orElseThrow(() -> new DomainRuleException(ErrorCode.API_INVALID_PARAM)); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
null 체크 추가 권장
source가 null인 경우를 명시적으로 처리하면 더 안전한 코드가 됩니다. 현재는 trim()에서 NullPointerException이 발생할 수 있습니다.
@Override
public T convert(String source) {
+ if (source == null) {
+ throw new DomainRuleException(ErrorCode.API_INVALID_PARAM);
+ }
String trimmedSource = source.trim();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public T convert(String source) { | |
| String trimmedSource = source.trim(); | |
| return Arrays.stream(targetType.getEnumConstants()) | |
| .filter(e -> e.getValue().equals(trimmedSource)) | |
| .findFirst() | |
| .orElseThrow(() -> new DomainRuleException(ErrorCode.API_INVALID_PARAM)); | |
| } | |
| public T convert(String source) { | |
| if (source == null) { | |
| throw new DomainRuleException(ErrorCode.API_INVALID_PARAM); | |
| } | |
| String trimmedSource = source.trim(); | |
| return Arrays.stream(targetType.getEnumConstants()) | |
| .filter(e -> e.getValue().equals(trimmedSource)) | |
| .findFirst() | |
| .orElseThrow(() -> new DomainRuleException(ErrorCode.API_INVALID_PARAM)); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/konkuk/chacall/global/common/converter/EnumValueConverter.java
around lines 28 to 36, add an explicit null check for the input source before
calling trim(): if source is null, throw the same
DomainRuleException(ErrorCode.API_INVALID_PARAM) (or handle per existing
contract), otherwise proceed to trim and perform the enum lookup as currently
implemented.
buzz0331
left a comment
There was a problem hiding this comment.
확인했습니다! converter 진짜 유용하네요! 감삼다
| @Component | ||
| public class EnumValueConverter implements ConverterFactory<String, EnumValue> { | ||
|
|
||
| @Override | ||
| public <T extends EnumValue> Converter<String, T> getConverter(Class<T> targetType) { | ||
| return new StringToEnumValueConverter<>(targetType); | ||
| } | ||
|
|
||
| private static final class StringToEnumValueConverter<T extends EnumValue> implements Converter<String, T> { | ||
| private final Class<T> targetType; | ||
|
|
||
| private StringToEnumValueConverter(Class<T> targetType) { | ||
| this.targetType = targetType; | ||
| } | ||
|
|
||
| @Override | ||
| public T convert(String source) { | ||
|
|
||
| String trimmedSource = source.trim(); | ||
|
|
||
| return Arrays.stream(targetType.getEnumConstants()) | ||
| .filter(e -> e.getValue().equals(trimmedSource)) | ||
| .findFirst() | ||
| .orElseThrow(() -> new DomainRuleException(ErrorCode.API_INVALID_PARAM)); | ||
| } | ||
| } |
There was a problem hiding this comment.
오 이러면 굳이 각 enum 마다 from 같은 것을 정의할 필요 없겠네요! 또 하나 알아갑니다~ 👍🏻
| @Parameter( | ||
| name = "paymentMethod", in = ParameterIn.QUERY, | ||
| description = "결제 방법(무관을 선택하면 필터 미적용)", | ||
| schema = @Schema( | ||
| type = "string", | ||
| example = "무관", | ||
| allowableValues = {"무관", "계좌이체", "카드"} | ||
| ) |
There was a problem hiding this comment.
저번부터 궁금했던 건데 allowableValues는 스웨거에 명세를 위한 파라미터인가요?? 저걸 추가하고 스웨거 명세를 봤는데 제가 명시한대로가 아닌 Enum이 가진 값이 뜨길래..
아니면 혹시 저게 유효성 검증 역할로 허용된 값만 들어오게 해주는 건가요??
There was a problem hiding this comment.
얘가 직접적으로 유효성 검증을 해주지는 않는 것 같아요.
그냥 문서화 용도라는 것 같아요! 저도 이게 정확히 어떤 흐름으로 동작하는지는 잘 모르겠네유.. 일단은 유효성 검증 용도는 아니고 스웨거 명세용인 것 같슴다
| INVALID_DATE_FORMAT | ||
| ))), | ||
| UPDATE_RESERVATION_STATUS(new LinkedHashSet<>(Set.of( | ||
| INVALID_ENUM_VALUE, |
#️⃣연관된 이슈
📝작업 내용
푸드트럭 목록 조회 API 를 구현하였습니다.
QueryDSL 도입
필터링 로직이 생각보다 많이 복잡해서 일반적인 JPQL 로는 처리가 굉장히 힘들어져서 QueryDSL 을 도입하였습니다.
그렇다고 해서 무조건 모든 리포지토리 기능을 QueryDSL 로 구현할 필요성은 전혀 없을 것 같고, 지금처럼 필요한 로직에만 선택적으로 사용하는 것이 낫지 않을까 생각합니다.
필터링 조건
검색어 -> 검색어를 기반으로, 해당 검색어가 푸드트럭의 이름에 포함되어있는지, 혹은 푸드트럭의 설명에 포함되어있는지를 따집니다.
지역 -> 지역의 regionCode 들을 전달받고, 해당 지역 코드로 시작하는 지역(startWith) 을 자신의 호출 가능 지역으로 갖는 푸드트럭을 조회합니다.
일정 -> 일정의 경우 yyyy.mm.dd~yyyy.mm.dd 형식의 문자열을 콤마로 구분하여 여러 일정 조건을 받을 수 있도록 해주었습니다. 이 때 시작일, 종료일을 효율적으로 관리하기 위해서 요청값을 DateRangeRequest 라는 DTO 로 매핑하여 받을 수 있도록 하였습니다.
수량 -> 수량의 경우 50인분 미만, 100인분 미만, 150인분 미만 과 같은 값들이 존재하는데, 이 때 특정 푸드트럭이 150인분 미만까지 커버가 가능하다고 명시해둔 경우, 필터링시에 100인분 미만을 조건으로 건다고 하더라도 150인분 미만은 100인분 미만까지도 커버 가능한 케이스라고 판단하여 조회시 결과에 포함되도록 구현해주었습니다.
메뉴 카테고리 -> 메뉴 카테고리의 enum 값들을 요청시에 받고, 그것들을 기반으로 DB 상에 저장되어있는 메뉴 카테고리 문자열 ( ex) 한식,양식,중식 ) 과 비교하여, 요청으로 받은 값들이 특정 푸드트럭의 메뉴 카테고리에 부분 문자열로써 포함되는지를 판단합니다.
전기, 결제 방법 -> 값 일치 여부만 판단합니다.
푸드트럭 상태 -> 푸드트럭 상태가 ON 으로 설정된 푸드트럭만을 조회합니다.
스크린샷 (선택)
아무 조건도 걸지 않았을 때의 쿼리
모든 조건을 걸었을 때의 쿼리
💬리뷰 요구사항(선택)
querydsl 관련 리포지토리들을 어느 위치에 만들어둘지 약간 고민입니다.
gpt의 경우 application / domain / presentation 외에 infrastructure 라는 패키지를 만들고, 그 하위에 querydsl 관련 리포지토리들을 만들라고 하긴 했는데, 그렇게 구현할 시, domain 하위의 repository 들과 서로 다른 위치에 존재하게 된다는 점에서 개인적으로 마음에 들지 않았습니다.
그래서 우선은 현재와 같이 domain/repository/querydsl 이라는 패키지를 만들어두긴 했는데, 이 패키지명도 좀 포괄적인 것 같아서 고민이네요.. 이 부분을 의논해보면 좋을 것 같아요
Summary by CodeRabbit