[FEAT] 예약 견적서 작성, 조회, 수정 api 구현#27
Conversation
…on-info # Conflicts: # src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java
Walkthrough예약 견적서 생성/조회/수정 API를 추가하고, 예약/푸드트럭 소유 검증을 도메인 메서드로 위임했습니다. 예약 도메인과 값 객체(ReservationInfo, ReservationDateList)를 확장/리네이밍하고 상태 Enum을 보강했습니다. 멤버/오너 서비스의 트랜잭션 구성을 재조정했으며, 여러 응답 DTO의 주소 값을 full address로 변경했습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Owner as Owner(UserId)
participant C as ReservationController
participant S as ReservationService
participant V1 as OwnerValidator
participant V2 as MemberValidator
participant I as ReservationInfoService
participant F as FoodTruckRepository
participant R as ReservationRepository
Owner->>C: POST /reservations (CreateReservationRequest)
C->>S: createReservation(request, ownerId)
S->>V1: validate/find owner(ownerId)
S->>V2: validate/find member(request.reservationUserId)
S->>I: createReservation(request, owner, member)
I->>F: findById(request.foodTruckId)
I->>R: save(Reservation.create(...))
R-->>I: reservationId
I-->>S: reservationId
S-->>C: ReservationIdResponse
C-->>Owner: 200 OK
sequenceDiagram
autonumber
actor User as Member/Owner(UserId)
participant C as ReservationController
participant S as ReservationService
participant V as MemberValidator
participant I as ReservationInfoService
participant R as ReservationRepository
participant D as Reservation(domain)
User->>C: GET /reservations/{id}
C->>S: getReservation(id, userId)
S->>V: validate/find user(userId)
S->>I: getReservation(id, user)
I->>R: findById(id)
R-->>I: Reservation
alt role == OWNER
I->>D: validateFoodTruckOwner(userId)
else role != OWNER
I->>D: validateReservedBy(userId)
end
I-->>S: ReservationResponse.of(Reservation)
S-->>C: ReservationResponse
C-->>User: 200 OK
sequenceDiagram
autonumber
actor User as Member(UserId)
participant C as ReservationController
participant S as ReservationService
participant V as MemberValidator
participant I as ReservationInfoService
participant R as ReservationRepository
participant D as Reservation(domain)
User->>C: PUT /reservations/{id} (UpdateReservationRequest)
C->>S: updateReservation(id, request, userId)
S->>V: validate/find user(userId)
S->>I: updateReservation(id, request, user)
I->>R: findById(id)
R-->>I: Reservation
opt access check
I->>D: validateFoodTruckOwner(userId) / validateReservedBy(userId)
end
I->>D: update(...)
I-->>S: reservationId
S-->>C: ReservationIdResponse
C-->>User: 200 OK
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests
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 |
There was a problem hiding this comment.
Actionable comments posted: 3
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/reservation/domain/value/ReservationDateList.java (1)
50-53: 날짜 범위 유효성(start <= end) 검증 추가 필요역전된 범위를 방치하면 이후 로직(포맷/필터)이 깨질 수 있습니다.
- LocalDate start = parseDot(dates[0].trim()); - LocalDate end = parseDot(dates[1].trim()); - list.add(new DateRange(start, end)); + LocalDate start = parseDot(dates[0].trim()); + LocalDate end = parseDot(dates[1].trim()); + if (end.isBefore(start)) { + throw new DomainRuleException(ErrorCode.INVALID_DATE_INPUT); + } + list.add(new DateRange(start, end));
🧹 Nitpick comments (21)
src/main/java/konkuk/chacall/domain/member/presentation/dto/response/MemberReservationHistoryResponse.java (1)
16-17: Swagger 예시 주소 갱신 제안상세주소 포함 가능성을 반영해 예시를 업데이트하면 좋습니다.
- @Schema(description = "예약 주소", example = "서울 광진구 화양동") + @Schema(description = "예약 주소", example = "서울 광진구 화양동 123-4 1층")src/main/java/konkuk/chacall/domain/member/presentation/dto/response/ReservationForRatingResponse.java (1)
26-27: Swagger 예시 업데이트 권장상세주소 포함을 반영한 예시로 갱신해 주세요.
- @Schema(description = "예약 주소", example = "서울 광진구 화양동") + @Schema(description = "예약 주소", example = "서울 광진구 화양동 123-4 1층")src/main/java/konkuk/chacall/domain/owner/application/reservation/OwnerReservationService.java (2)
68-75: 고객 조회 누락/빈 목록 처리 보강 필요
- reservations가 빈 경우 쿼리 스킵 권장.
- userRepository로부터 일부 회원이 누락되면
customerMap.get(...)가 null이 되어 NPE 가능성이 있습니다(OwnerReservationHistoryResponse.of에 null 전달).private Map<Long, User> getCustomerMap(List<Reservation> reservations) { - List<Long> customerIds = reservations.stream() - .map(reservation -> reservation.getMember().getUserId()) - .toList(); - - return userRepository.findAllByUserIdInAndRoleAndStatus(customerIds, Role.MEMBER, BaseStatus.ACTIVE).stream() - .collect(Collectors.toMap(User::getUserId, user -> user)); + List<Long> customerIds = reservations.stream() + .map(r -> r.getMember().getUserId()) + .distinct() + .toList(); + if (customerIds.isEmpty()) return Map.of(); + + Map<Long, User> map = userRepository + .findAllByUserIdInAndRoleAndStatus(customerIds, Role.MEMBER, BaseStatus.ACTIVE) + .stream() + .collect(Collectors.toMap(User::getUserId, u -> u)); + return map; } private List<OwnerReservationHistoryResponse> mapToReservationHistory(List<Reservation> reservations, Map<Long, User> customerMap) { return reservations.stream() .map(reservation -> { - User customer = customerMap.get(reservation.getMember().getUserId()); + User customer = customerMap.get(reservation.getMember().getUserId()); + if (customer == null) { + // 누락된 사용자는 마스킹/기본값 처리 또는 스킵 + throw new EntityNotFoundException(ErrorCode.USER_NOT_FOUND); + } return OwnerReservationHistoryResponse.of(reservation, customer); }) .toList(); }Also applies to: 80-86
13-15: 불필요한 import 제거
BusinessException가 더 이상 사용되지 않습니다.-import konkuk.chacall.global.common.exception.BusinessException;src/main/java/konkuk/chacall/domain/reservation/domain/value/ReservationDateList.java (3)
32-34: 불변 리스트 중복 래핑
of()에서 이미List.copyOf로 불변을 보장하므로Collections.unmodifiableList는 중복입니다. 직접 반환해도 됩니다.- public List<DateRange> getRanges() { - return Collections.unmodifiableList(ranges); - } + public List<DateRange> getRanges() { + return ranges; + }
28-30: Null 불가 보장 시 불필요한 null 체크 제거 제안
ranges는of()에서 null이면List.of()로 치환되어 null 아님이 보장됩니다. 간소화 가능.- public boolean isEmpty() { - return ranges == null || ranges.isEmpty(); - } + public boolean isEmpty() { + return ranges.isEmpty(); + }
8-8: 사용되지 않는 Lombok import
@Getter를 사용하지 않으므로 제거하세요.-import lombok.Getter;src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruck.java (1)
78-86: 도메인 소유자 검증 도입 좋습니다엔티티 스스로 규칙을 보장하게 되어 응집도가 높아졌습니다.
isOwnedBy의 가시성을 package-private로 열어 도메인 단위 테스트에서 직접 검증 가능하게 하는 것도 고려해 보세요.- private boolean isOwnedBy(Long ownerId) { + /* package-private */ boolean isOwnedBy(Long ownerId) { return this.owner.getUserId().equals(ownerId); }src/main/java/konkuk/chacall/domain/member/application/reservation/MemberReservationService.java (1)
10-10: 불필요한 import 제거
BusinessException미사용입니다.-import konkuk.chacall.global.common.exception.BusinessException;src/main/java/konkuk/chacall/domain/reservation/presentation/ReservationController.java (1)
43-54: 조회 API의 권한 검증 로직 개선 고려예약 조회 시 사장님과 일반 유저를 구분하는 로직이 서비스 레이어에서
user.getRole()로 처리되고 있습니다. 그러나 컨트롤러에서는 모든 사용자를userId로 받고 있어, 권한에 따른 처리가 명확하지 않습니다.권한별로 다른 엔드포인트를 제공하거나, 최소한 OpenAPI 문서에 권한별 동작 차이를 명시하는 것을 고려해보세요:
@Operation( summary = "예약 견적서 조회", - description = "사장님이 작성한 예약 견적서를 조회합니다. 사장님, 일반 유저 모두 조회 가능합니다." + description = "사장님이 작성한 예약 견적서를 조회합니다. 사장님은 본인 푸드트럭의 예약을, 일반 유저는 본인이 예약한 건만 조회 가능합니다." )src/main/java/konkuk/chacall/domain/reservation/application/ReservationService.java (1)
38-43: updateReservation 메서드의 권한 검증 불일치 가능성
getReservation과 동일하게memberValidator만 사용하여 검증하고 있어, Owner가 자신의 푸드트럭 예약을 수정하려고 할 때 문제가 발생할 수 있습니다.Owner와 Member를 모두 처리할 수 있도록 수정이 필요합니다:
@Transactional public Long updateReservation(Long reservationId, UpdateReservationRequest request, Long userId) { - User user = memberValidator.validateAndGetMember(userId); + // UserValidator 또는 UserRepository를 통해 Role에 관계없이 유저 조회 + User user = userValidator.validateAndGetUser(userId); return reservationInfoService.updateReservation(reservationId, request, user); }src/main/java/konkuk/chacall/domain/reservation/presentation/dto/response/ReservationResponse.java (1)
17-21: 정규식 패턴이 중복 정의되어 있음
reservationDates필드의 검증 패턴이 여기서 정의되어 있는데, 이는 응답 DTO이므로 검증이 필요하지 않습니다. 요청 DTO에만 검증을 적용하는 것이 적절합니다.응답 DTO에서는 검증 어노테이션을 제거하세요:
@Schema(description = "예약 날짜 (형식: YYYY.MM.DD ~ YYYY.MM.DD)", example = "[\"2025.09.20 ~ 2025.09.20\", \"2025.09.25 ~ 2025.09.25\"]") -List<@Pattern( - regexp = "^\\d{4}\\.\\d{2}\\.\\d{2} ~ \\d{4}\\.\\d{2}\\.\\d{2}$", - message = "예약 날짜 형식이 올바르지 않습니다. (예: 2025.09.20 ~ 2025.09.20)" -) String> reservationDates, +List<String> reservationDates,src/main/java/konkuk/chacall/domain/reservation/domain/value/ReservationInfo.java (9)
15-19: Embeddable에 공개 AllArgsConstructor 노출은 남용 여지 — private로 제한 권고값객체 무결성 보존 관점에서 생성 경로를 Builder로만 고정하는 편이 안전합니다.
-@AllArgsConstructor +@AllArgsConstructor(access = lombok.AccessLevel.PRIVATE)
22-27: 주소 필드에 Bean Validation 추가 권고(@notblank)DB 제약(null)만으로는 빈 문자열(" ")을 막지 못합니다. 입력단 유효성 일관성을 위해 @notblank를 권고합니다.
-@Column(nullable = false) -private String address; // 주소 (시/동/구) +@Column(nullable = false) +@jakarta.validation.constraints.NotBlank +private String address; // 주소 (시/구/동) -@Column(nullable = false) -private String detailAddress; // 상세 주소 +@Column(nullable = false) +@jakarta.validation.constraints.NotBlank +private String detailAddress; // 상세 주소
32-35: 운영시간을 문자열로 보관하는 대신 값객체화를 고려하세요"HH:mm ~ HH:mm" 패턴 검증/정렬/비교가 빈번할 경우 OperationHourRange(시작/종료 LocalTime) 값객체가 안정적입니다.
38-40: 예약금 음수/널 방지: @NotNull, @min(0) 권고도메인 규칙 강화 차원에서 Bean Validation 추가를 권고합니다.
-@Column(nullable = false) -private Integer deposit; // 예약금 +@Column(nullable = false) +@jakarta.validation.constraints.NotNull +@jakarta.validation.constraints.Min(0) +private Integer deposit; // 예약금
47-57: DateTimeFormatter 상수화 및 널/빈 목록 안전 처리
- Formatter 재생성 비용/중복 제거
- reservationDates null/empty일 때 빈 리스트 반환
- public List<String> getFormattedDateTimeInfos() { - - DateTimeFormatter DOT = DateTimeFormatter.ofPattern("yyyy.MM.dd"); - - return this.reservationDates.getRanges().stream() + private static final DateTimeFormatter DOT = DateTimeFormatter.ofPattern("yyyy.MM.dd"); + + public List<String> getFormattedDateTimeInfos() { + if (this.reservationDates == null || this.reservationDates.isEmpty()) return List.of(); + return this.reservationDates.getRanges().stream() .map(date -> date.startDate().format(DOT) + " ~ " + date.endDate().format(DOT) + " " + this.operationHour) .toList(); } - public List<String> getFormattedDateInfos() { - DateTimeFormatter DOT = DateTimeFormatter.ofPattern("yyyy.MM.dd"); - - return this.reservationDates.getRanges().stream() + public List<String> getFormattedDateInfos() { + if (this.reservationDates == null || this.reservationDates.isEmpty()) return List.of(); + return this.reservationDates.getRanges().stream() .map(date -> date.startDate().format(DOT) + " ~ " + date.endDate().format(DOT)) .toList(); }Also applies to: 59-68
70-72: full address 조합 시 공백/널 안전 처리detailAddress가 비거나 null이면 말줄임/이중 공백이 생깁니다. 간단히 정리하세요.
- public String getFullAddress() { - return this.address + " " + this.detailAddress; - } + public String getFullAddress() { + String base = address == null ? "" : address.trim(); + if (detailAddress == null || detailAddress.isBlank()) return base; + return base.isEmpty() ? detailAddress.trim() : base + " " + detailAddress.trim(); + }
78-80: 예약금 포맷팅: 메서드명 개선 + 천단위 구분자가독성을 위해 NumberFormat 사용과 메서드명 정리를 권장합니다.
- public String parsingReservationDeposit() { - return deposit + "원"; - } + public String formatDeposit() { + java.text.NumberFormat nf = java.text.NumberFormat.getInstance(java.util.Locale.KOREA); + return nf.format(deposit) + "원"; + }
82-100: update 파라미터 네이밍 일관성(reservationDate → reservationDates) 및 기본 검증 권고
- 필드명과 일치하도록 복수형으로 통일
- 업데이트 경로에서도 널/음수 방지(Bean Validation로 1차 방어, 필요 시 도메인 예외 추가)
- public void updateReservationInfo( - String reservationAddress, - String reservationDetailAddress, - ReservationDateList reservationDate, - String operationHour, - String menu, - Integer reservationDeposit, - boolean isUseElectricity, - String etcRequest - ) { + public void updateReservationInfo( + String reservationAddress, + String reservationDetailAddress, + ReservationDateList reservationDates, + String operationHour, + String menu, + Integer reservationDeposit, + boolean isUseElectricity, + String etcRequest + ) { - this.address = reservationAddress; - this.detailAddress = reservationDetailAddress; - this.reservationDates = reservationDate; + this.address = reservationAddress; + this.detailAddress = reservationDetailAddress; + this.reservationDates = reservationDates; this.operationHour = operationHour; this.menu = menu; this.deposit = reservationDeposit; this.isUseElectricity = isUseElectricity; this.etcRequest = etcRequest; }
74-76: 메서드명 오탈자 수정(및 도메인에서의 표현 로직 제거 권장)parsingIsUserElectricity()의 "User"는 오타입니다 — 빠른 수정: 메서드명 formatUseElectricity()로 변경하고 호출부 2곳 업데이트. 권장: 도메인은 boolean 접근자(isUseElectricity())만 제공하고 DTO에서 '가능'/'불가능'으로 포맷하세요.
수정 필요 호출부: src/main/java/konkuk/chacall/domain/owner/presentation/dto/response/OwnerReservationDetailResponse.java:53, src/main/java/konkuk/chacall/domain/member/presentation/dto/response/MemberReservationDetailResponse.java:52.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (26)
src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruck.java(2 hunks)src/main/java/konkuk/chacall/domain/member/application/MemberService.java(3 hunks)src/main/java/konkuk/chacall/domain/member/application/foodtruck/SavedFoodTruckService.java(0 hunks)src/main/java/konkuk/chacall/domain/member/application/rating/RatingService.java(0 hunks)src/main/java/konkuk/chacall/domain/member/application/reservation/MemberReservationService.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/owner/application/myfoodtruck/MyFoodTruckService.java(1 hunks)src/main/java/konkuk/chacall/domain/owner/application/reservation/OwnerReservationService.java(1 hunks)src/main/java/konkuk/chacall/domain/owner/presentation/dto/response/OwnerReservationDetailResponse.java(1 hunks)src/main/java/konkuk/chacall/domain/owner/presentation/dto/response/OwnerReservationHistoryResponse.java(1 hunks)src/main/java/konkuk/chacall/domain/reservation/application/ReservationService.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(3 hunks)src/main/java/konkuk/chacall/domain/reservation/domain/value/ReservationDateList.java(1 hunks)src/main/java/konkuk/chacall/domain/reservation/domain/value/ReservationInfo.java(2 hunks)src/main/java/konkuk/chacall/domain/reservation/domain/value/ReservationStatus.java(1 hunks)src/main/java/konkuk/chacall/domain/reservation/presentation/ReservationController.java(1 hunks)src/main/java/konkuk/chacall/domain/reservation/presentation/dto/request/CreateReservationRequest.java(1 hunks)src/main/java/konkuk/chacall/domain/reservation/presentation/dto/request/UpdateReservationRequest.java(1 hunks)src/main/java/konkuk/chacall/domain/reservation/presentation/dto/response/ReservationIdResponse.java(1 hunks)src/main/java/konkuk/chacall/domain/reservation/presentation/dto/response/ReservationResponse.java(1 hunks)src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java(1 hunks)src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java(2 hunks)src/main/resources/application-dev.yml(1 hunks)
💤 Files with no reviewable changes (2)
- src/main/java/konkuk/chacall/domain/member/application/rating/RatingService.java
- src/main/java/konkuk/chacall/domain/member/application/foodtruck/SavedFoodTruckService.java
🧰 Additional context used
🧬 Code graph analysis (4)
src/main/java/konkuk/chacall/domain/member/application/MemberService.java (2)
src/main/java/konkuk/chacall/domain/reservation/application/ReservationService.java (1)
Service(14-44)src/main/java/konkuk/chacall/domain/owner/application/OwnerService.java (1)
RequiredArgsConstructor(19-139)
src/main/java/konkuk/chacall/domain/reservation/application/reservationinfo/ReservationInfoService.java (2)
src/main/java/konkuk/chacall/global/common/exception/EntityNotFoundException.java (1)
EntityNotFoundException(6-11)src/main/java/konkuk/chacall/domain/reservation/application/ReservationService.java (1)
Service(14-44)
src/main/java/konkuk/chacall/domain/reservation/domain/value/ReservationInfo.java (2)
src/main/java/konkuk/chacall/domain/reservation/domain/value/ReservationDateList.java (1)
AllArgsConstructor(17-82)src/main/java/konkuk/chacall/domain/reservation/domain/model/Reservation.java (1)
Getter(17-144)
src/main/java/konkuk/chacall/domain/reservation/application/ReservationService.java (2)
src/main/java/konkuk/chacall/domain/reservation/application/reservationinfo/ReservationInfoService.java (1)
Service(17-82)src/main/java/konkuk/chacall/domain/member/application/MemberService.java (1)
Service(16-75)
🔇 Additional comments (31)
src/main/java/konkuk/chacall/domain/member/application/MemberService.java (5)
14-14: 필수 import 추가 승인클래스 레벨에서 트랜잭션 관리를 위한 Transactional import가 적절히 추가되었습니다.
18-18: 읽기 전용 기본 트랜잭션 설정 승인클래스 레벨에 @transactional(readOnly = true)를 설정하여 모든 메서드에 기본적으로 읽기 전용 트랜잭션을 적용하는 것은 적절한 패턴입니다. 메서드 레벨의 @transactional 어노테이션이 클래스 레벨 설정을 오버라이드할 수 있습니다.
이는 현재 PR 전반에 걸친 트랜잭션 관리 개선 작업과 일치하며, 다른 서비스(ReservationService, OwnerService 등)와 일관된 패턴을 유지합니다.
27-27: 쓰기 작업을 위한 트랜잭션 오버라이드 승인클래스 레벨의 읽기 전용 설정을 메서드 레벨 @transactional로 오버라이드하여 쓰기 작업을 가능하게 하는 구현이 올바릅니다.
43-43: 쓰기 작업을 위한 트랜잭션 오버라이드 승인평점 등록과 같은 쓰기 작업을 위해 @transactional로 클래스 레벨의 readOnly 설정을 적절히 오버라이드했습니다.
36-41: 읽기 전용 트랜잭션 컨텍스트에서 실행 승인getSavedFoodTrucks 메서드는 클래스 레벨의 readOnly = true 트랜잭션 컨텍스트에서 실행되어 읽기 최적화가 적용됩니다.
src/main/resources/application-dev.yml (1)
13-13: 개발 환경 설정 확인ddl-auto가
create로 변경되어 애플리케이션 시작 시 스키마가 초기화됩니다. 개발 데이터 손실 가능성을 인지하고 의도적인 변경인지 확인 부탁드립니다.src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java (1)
59-59: LGTM!새로운 에러 코드가 적절하게 추가되었습니다. 소유자가 본인 푸드트럭을 예약하는 것을 방지하는 비즈니스 규칙을 명확히 표현하고 있습니다.
src/main/java/konkuk/chacall/domain/reservation/domain/value/ReservationStatus.java (1)
10-16: 예약 상태 구조 개선요청과 완료를 구분하는 상태 추가로 예약 생명주기를 더 세밀하게 관리할 수 있게 되었습니다. 상태 전이 순서도 논리적으로 잘 배치되어 있습니다.
src/main/java/konkuk/chacall/domain/reservation/presentation/dto/request/UpdateReservationRequest.java (1)
8-46: LGTM!검증 애노테이션과 스키마 문서화가 잘 구성되어 있습니다. 특히 날짜 형식과 운영시간 형식에 대한 정규식 패턴이 명확하게 정의되어 있고, 에러 메시지도 사용자 친화적입니다.
src/main/java/konkuk/chacall/domain/reservation/presentation/dto/request/CreateReservationRequest.java (1)
8-54: LGTM!검증 로직이 포괄적으로 구현되어 있습니다. 각 필드의 검증 규칙과 에러 메시지가 명확하며, API 문서화도 잘 되어 있습니다. 특히 날짜와 시간 형식에 대한 정규식 패턴이 정확합니다.
src/main/java/konkuk/chacall/domain/owner/presentation/dto/response/OwnerReservationDetailResponse.java (1)
48-48: 주소 정보 접근 방식 통일
getFullAddress()메서드 사용으로 주소 정보 접근 방식이 통일되었습니다. ReservationInfo의 주소 필드 리팩토링과 일치하는 변경입니다.src/main/java/konkuk/chacall/domain/owner/application/myfoodtruck/MyFoodTruckService.java (1)
58-58: 도메인 레벨 검증으로 캡슐화 개선소유권 검증 로직이 도메인 객체로 이동하여 비즈니스 규칙이 적절한 위치에 캡슐화되었습니다. 도메인 주도 설계 원칙에 부합하는 좋은 리팩토링입니다.
src/main/java/konkuk/chacall/domain/member/presentation/dto/response/MemberReservationDetailResponse.java (1)
47-47: LGTM!다른 응답 DTO들과 일관성 있게
getFullAddress()메서드를 사용하도록 변경되었습니다. 코드베이스 전체의 주소 접근 방식이 통일되어 유지보수성이 향상되었습니다.src/main/java/konkuk/chacall/domain/owner/presentation/dto/response/OwnerReservationHistoryResponse.java (2)
32-32: FullAddress 사용 변경 👍도메인에 위임된 주소 포맷(getFullAddress) 사용 일관성 좋습니다.
16-17: Swagger 예시: 상세주소 포함으로 갱신 필요상세주소(건물번호·호수 등)가 포함될 수 있으므로 @Schema의 example을 아래와 같이 업데이트하세요.
- @Schema(description = "예약 주소", example = "서울 광진구 화양동") + @Schema(description = "예약 주소", example = "서울 광진구 화양동 123-4 1층")getFullAddress / getFormattedDateTimeInfos 구현 존재 여부와 "최대 2개" 제한 보장 여부를 확인해 주세요. (검색 스크립트 예시)
#!/bin/bash rg -n -C3 --type java --no-ignore -S "getFullAddress\(" || true rg -n -C3 --type java --no-ignore -S "getFormattedDateTimeInfos\(" || truesrc/main/java/konkuk/chacall/domain/member/presentation/dto/response/MemberReservationHistoryResponse.java (1)
30-30: FullAddress 사용으로 변경된 점 OK응답 일관성 측면에서 긍정적입니다.
src/main/java/konkuk/chacall/domain/member/presentation/dto/response/ReservationForRatingResponse.java (1)
41-41: FullAddress 사용 반영 👍다른 DTO들과의 정합성 확보되었습니다.
src/main/java/konkuk/chacall/domain/owner/application/reservation/OwnerReservationService.java (2)
51-51: 소유자 검증을 도메인으로 위임한 점 Good서비스 레이어 단순화와 규칙 응집이 좋아졌습니다. 예외 매핑(HTTP 상태/에러코드)이 기존과 동일하게 동작하는지 한 번만 확인 부탁드립니다.
61-63: 커서 페이지네이션 정렬 안정성 확인
findOwnerReservationsByStatusWithCursor가 일관된 정렬(예: reservation_id DESC)과 커서 조건을 함께 사용 중인지 확인 필요합니다. 불안정 정렬이면 중복/누락 가능성이 있습니다.src/main/java/konkuk/chacall/domain/member/application/reservation/MemberReservationService.java (2)
43-43: 예약자 검증 도메인 위임 👍규칙 집중화 OK. 트랜잭션이 파사드에서 열려 있는지(특히 LAZY 필드 접근 시)만 확인 부탁드립니다.
28-30: 커서 기반 조회의 정렬/커서 조건 점검
PageRequest.of(0, pageSize)와 커서 조건이 함께 안정된 정렬 기준으로 동작하는지 확인 필요합니다.src/main/java/konkuk/chacall/domain/reservation/presentation/dto/response/ReservationIdResponse.java (1)
9-11: 단일 ID 응답 DTO 도입 👍명세 명확해지고 추후 확장 용이합니다.
Jackson 설정(레코드 직렬화)이 이미 활성화되어 있는지 한번만 확인해 주세요.
src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java (1)
127-147: 예약 API 에러 코드 정의 확인새로 추가된 예약 관련 Swagger 응답 설명이 적절하게 정의되어 있습니다. 각 API별로 발생 가능한 에러 코드들이 잘 매핑되어 있습니다.
src/main/java/konkuk/chacall/domain/reservation/presentation/dto/response/ReservationResponse.java (1)
38-50: of 메서드 구현이 적절함Reservation 엔티티를 응답 DTO로 변환하는 팩토리 메서드가 잘 구현되어 있습니다. ReservationInfo의 각 필드를 적절히 매핑하고 있습니다.
src/main/java/konkuk/chacall/domain/reservation/domain/model/Reservation.java (3)
80-114: create 메서드 구현이 잘 되어 있음예약 생성 팩토리 메서드가 적절하게 구현되어 있습니다. 유효성 검증, ReservationInfo 생성, 초기 상태 설정 등이 모두 적절합니다.
123-143: update 메서드 구현이 적절함예약 정보 수정 메서드가 ReservationInfo에 위임하여 적절하게 구현되어 있습니다.
116-121: validateCreateReservation 검증 로직 — FoodTruck.validateOwner 구현 확인 필요validateCreateReservation은 소유자 검증과 본인 예약 차단을 적절히 구현하고 있습니다. 다만 ast-grep 실행 결과 validateOwner 구현을 찾지 못해(출력 없음) 해당 메서드의 ID 비교 방식·null 처리·예외 던짐 여부를 직접 확인해 주세요. 확인용 명령(권장): rg -n --hidden -S '\bvalidateOwner\b' || rg -n --hidden -S 'class\s+FoodTruck'
src/main/java/konkuk/chacall/domain/reservation/application/ReservationService.java (1)
32-36: 권한 검증 불일치: Member vs Owner 확인 필요
ReservationService#getReservation이 memberValidator.validateAndGetMember(userId)로 Member만 검증합니다. ReservationInfoService가 Owner 권한도 처리한다면 권한 불일치 또는 권한 누락이 발생할 수 있으니 MemberValidator/OwnerValidator 구현을 확인하고 getReservation에 적절한 권한 분기 또는 Owner 허용을 적용하세요. 검증 스크립트가 출력되지 않아 Validator 구현을 확인할 수 없습니다.src/main/java/konkuk/chacall/domain/reservation/presentation/ReservationController.java (1)
28-41: 컨트롤러에서 Owner 권한 검증 누락 — 서비스에서 검증 중 (검증 완료)결과: @userid는 UserIdArgumentResolver로 주입(WebConfig에 등록)되며 ReservationController는 ownerId만 주입하고 컨트롤러 레벨의 역할(Owner) 검증은 없습니다. 권한 검증은 ReservationService.createReservation에서 OwnerValidator로 수행됩니다.
권고: 즉시 거부를 원하면 컨트롤러에 역할 검사(@PreAuthorize 또는 별도 체크) 추가하거나, 현재 서비스에서 던지는 권한 예외가 403 등으로 적절히 매핑되는지 확인하십시오.
참고 파일: src/main/java/konkuk/chacall/domain/reservation/presentation/ReservationController.java, src/main/java/konkuk/chacall/global/common/security/resolver/UserIdArgumentResolver.java, src/main/java/konkuk/chacall/global/config/WebConfig.java, src/main/java/konkuk/chacall/domain/reservation/application/ReservationService.java, src/main/java/konkuk/chacall/domain/owner/application/validator/OwnerValidator.java
src/main/java/konkuk/chacall/domain/reservation/domain/value/ReservationInfo.java (2)
28-31: 확인: ReservationDateListConverter가 빈 문자열/공백을 안전히 처리함convertToEntityAttribute에서 dbData == null || dbData.isBlank()일 때 ReservationDateList.of(List.of())를 반환하고, convertToDatabaseColumn은 attribute == null || attribute.isEmpty()일 때 빈 문자열("")을 반환합니다. 빈 문자열/공백으로 인한 NPE나 역직렬화 문제 없음.
53-56: 확인 완료 — Java 17(toolchain) 사용 중이므로 Stream.toList() 안전
build.gradle에 JavaLanguageVersion.of(17)으로 toolchain이 설정되어 있어 toList() (JDK16+) 사용에 문제가 없습니다.
| public ReservationResponse getReservation(Long reservationId, User user) { | ||
| Reservation reservation = reservationRepository.findById(reservationId) | ||
| .orElseThrow(() -> new EntityNotFoundException(ErrorCode.RESERVATION_NOT_FOUND)); | ||
|
|
||
| // 예약 소유자 또는 푸드트럭 소유자인지 확인 | ||
| if(user.getRole().equals(Role.OWNER)) { | ||
| reservation.validateFoodTruckOwner(user.getUserId()); | ||
| } else { | ||
| reservation.validateReservedBy(user.getUserId()); | ||
| } | ||
|
|
||
| return ReservationResponse.of(reservation); | ||
| } |
There was a problem hiding this comment.
권한 검증 로직의 일관성 개선 필요
getReservation 메서드에서 Owner와 Member를 구분하는 로직이 있지만, Owner가 예약자로서 조회하려는 경우를 제대로 처리하지 못할 수 있습니다. Owner가 다른 사장님의 푸드트럭을 예약한 경우, validateFoodTruckOwner에서 실패할 가능성이 있습니다.
다음과 같이 수정하여 Owner가 예약자로서도 조회할 수 있도록 개선하세요:
// 예약 소유자 또는 푸드트럭 소유자인지 확인
if(user.getRole().equals(Role.OWNER)) {
- reservation.validateFoodTruckOwner(user.getUserId());
+ // Owner가 푸드트럭 소유자이거나 예약자인 경우 모두 허용
+ try {
+ reservation.validateFoodTruckOwner(user.getUserId());
+ } catch (DomainRuleException e) {
+ reservation.validateReservedBy(user.getUserId());
+ }
} else {
reservation.validateReservedBy(user.getUserId());
}📝 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 ReservationResponse getReservation(Long reservationId, User user) { | |
| Reservation reservation = reservationRepository.findById(reservationId) | |
| .orElseThrow(() -> new EntityNotFoundException(ErrorCode.RESERVATION_NOT_FOUND)); | |
| // 예약 소유자 또는 푸드트럭 소유자인지 확인 | |
| if(user.getRole().equals(Role.OWNER)) { | |
| reservation.validateFoodTruckOwner(user.getUserId()); | |
| } else { | |
| reservation.validateReservedBy(user.getUserId()); | |
| } | |
| return ReservationResponse.of(reservation); | |
| } | |
| public ReservationResponse getReservation(Long reservationId, User user) { | |
| Reservation reservation = reservationRepository.findById(reservationId) | |
| .orElseThrow(() -> new EntityNotFoundException(ErrorCode.RESERVATION_NOT_FOUND)); | |
| // 예약 소유자 또는 푸드트럭 소유자인지 확인 | |
| if(user.getRole().equals(Role.OWNER)) { | |
| // Owner가 푸드트럭 소유자이거나 예약자인 경우 모두 허용 | |
| try { | |
| reservation.validateFoodTruckOwner(user.getUserId()); | |
| } catch (DomainRuleException e) { | |
| reservation.validateReservedBy(user.getUserId()); | |
| } | |
| } else { | |
| reservation.validateReservedBy(user.getUserId()); | |
| } | |
| return ReservationResponse.of(reservation); | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/konkuk/chacall/domain/reservation/application/reservationinfo/ReservationInfoService.java
around lines 44-56, the current role check only validates Owner as the foodtruck
owner which blocks Owners who made a reservation on another owner's truck.
Change the logic so Owners can access a reservation if they are either the
reservation maker or the foodtruck owner: for an Owner user, first check if they
are the reserving user and call validateReservedBy if so, otherwise call
validateFoodTruckOwner; for non-Owner users keep calling validateReservedBy. Use
the existing validation methods (validateReservedBy and validateFoodTruckOwner)
accordingly.
| public Long updateReservation(Long reservationId, UpdateReservationRequest request, User user) { | ||
| Reservation reservation = reservationRepository.findById(reservationId) | ||
| .orElseThrow(() -> new EntityNotFoundException(ErrorCode.RESERVATION_NOT_FOUND)); | ||
|
|
||
| // 예약 소유자 또는 푸드트럭 소유자인지 확인 | ||
| if(user.getRole().equals(Role.OWNER)) { | ||
| reservation.validateFoodTruckOwner(user.getUserId()); | ||
| } else { | ||
| reservation.validateReservedBy(user.getUserId()); | ||
| } | ||
|
|
||
| reservation.update( | ||
| request.address(), | ||
| request.detailAddress(), | ||
| request.reservationDates(), | ||
| request.operationHour(), | ||
| request.menu(), | ||
| request.deposit(), | ||
| request.isUseElectricity(), | ||
| request.etcRequest() | ||
| ); | ||
|
|
||
| return reservation.getReservationId(); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
updateReservation 메서드의 권한 검증 로직 중복
getReservation과 동일한 권한 검증 로직이 중복되어 있습니다. 또한 동일한 문제점을 가지고 있습니다.
권한 검증 로직을 별도 메서드로 추출하여 재사용하세요:
+private void validateReservationAccess(Reservation reservation, User user) {
+ if(user.getRole().equals(Role.OWNER)) {
+ // Owner가 푸드트럭 소유자이거나 예약자인 경우 모두 허용
+ try {
+ reservation.validateFoodTruckOwner(user.getUserId());
+ } catch (DomainRuleException e) {
+ reservation.validateReservedBy(user.getUserId());
+ }
+ } else {
+ reservation.validateReservedBy(user.getUserId());
+ }
+}
public ReservationResponse getReservation(Long reservationId, User user) {
Reservation reservation = reservationRepository.findById(reservationId)
.orElseThrow(() -> new EntityNotFoundException(ErrorCode.RESERVATION_NOT_FOUND));
- // 예약 소유자 또는 푸드트럭 소유자인지 확인
- if(user.getRole().equals(Role.OWNER)) {
- reservation.validateFoodTruckOwner(user.getUserId());
- } else {
- reservation.validateReservedBy(user.getUserId());
- }
+ validateReservationAccess(reservation, user);
return ReservationResponse.of(reservation);
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
src/main/java/konkuk/chacall/domain/reservation/application/reservationinfo/ReservationInfoService.java
around lines 58-81, the role-based permission check in updateReservation is
duplicated with getReservation; extract this logic into a single private helper
(e.g., validateReservationAccess(Reservation reservation, User user)) and call
it from both getReservation and updateReservation. The helper should check
user.getRole() and, for OWNER, call
reservation.validateFoodTruckOwner(user.getUserId()), otherwise call
reservation.validateReservedBy(user.getUserId()); then replace the duplicated
block in updateReservation with a call to the new helper and ensure imports and
visibility are correct.
ksg1227
left a comment
There was a problem hiding this comment.
야무지네요 👍🏻
거의 도메인 역할 분담에 달인이 다 되셨슈
세부 기능들은 사실상 완벽한 것 같고, 궁금한 부분이랑 한 가지 개선해보면 어떨까 싶은 부분 리뷰 담겼으니 확인부탁드림다~
| // 본인이 푸드트럭 소유자인지 검증 | ||
| public void validateFoodTruckOwner(Long ownerId) { | ||
| if (isForFoodTruckOwnedBy(ownerId)) { | ||
| throw new DomainRuleException(RESERVATION_NOT_OWNED); | ||
| } | ||
| } |
There was a problem hiding this comment.
p2 : 이 부분은 개인적으로 코드래빗 리뷰처럼 코드의 의미가 살짝 모호하다는 느낌이 드는 것 같습니다!
isForFoodTruckOwnedBy -> 푸드트럭을 소유했다면 true 반환 / 소유하지 않았다면 false 반환 이런 형식의 반환값을 가질 것이라 예측되는데, 실제로는 푸드트럭을 소유한 경우 false 반환 / 소유하지 않은 경우 true 를 반환하는 것 같아서
private boolean isForFoodTruckOwnedBy(Long ownerId) {
return this.foodTruck.getOwner().getUserId().equals(ownerId);
}
...
public void validateFoodTruckOwner(Long ownerId) {
if (!isForFoodTruckOwnedBy(ownerId)) { <- 여기에 ! 붙이기
throw new DomainRuleException(RESERVATION_NOT_OWNED);
}
}위와 같은 형태로 구현해두는 것이 메서드 이름의 의미상 좀 더 명확하지 않나 생각합니다!
There was a problem hiding this comment.
맞네요! 리팩토링하면서 놓쳤던 것 같습니다! 굿굿
| // 예약 소유자 또는 푸드트럭 소유자인지 확인 | ||
| if(user.getRole().equals(Role.OWNER)) { | ||
| reservation.validateFoodTruckOwner(user.getUserId()); | ||
| } else { | ||
| reservation.validateReservedBy(user.getUserId()); | ||
| } |
| .reservationId(null) | ||
| .reservationStatus(ReservationStatus.PENDING) // 기본 상태: 예약 대기 | ||
| .reservationInfo(reservationInfo) | ||
| .pdfUrl(null) |
There was a problem hiding this comment.
제 기억으로는 예약 객체를 생성하는 시점은 예약 견적서가 발급되는 시점인 것 같은데 맞나요?
그렇다면 이 때에는 pdf 가 생성되지 않은 채로 예약 객체가 생성되는 것인지 궁금합니다!
There was a problem hiding this comment.
일단 지금 생각은 처음에는 pdfUrl을 null로 두고 처음 다운로드가 일어나면 그때부터 pdfUrl을 주입할 생각이긴 했습니다!
제가 생각한 pdf 다운로드 API 흐름은 다음과 같습니다.
- pdfUrl = null이라면, html을 pdf로 변환 -> s3에 pdf 업로드 -> pdfUrl을 테이블에 업데이트 -> pdfUrl 반환
- pdfUrl != null이라면, pdfUrl 반환
There was a problem hiding this comment.
음 근데 생각해보니까 예약 수정 같은 api가 호출되면 그때마다 pdf를 갱신해줘야겠네요.. 그러면 pdfUrl이 null이 아니더라도 매번 pdf를 변환해서 s3에 업로드해야하는 이슈가 발생하겠네요..
그러면.. 예약 견적서 작성 또는 수정시에 pdf까지 변환해서 s3에 업로드를 해놔야할까요?? 살짝 헷갈리네요
There was a problem hiding this comment.
일단 지금 생각은 처음에는 pdfUrl을 null로 두고 처음 다운로드가 일어나면 그때부터 pdfUrl을 주입할 생각이긴 했습니다!
제가 생각한 pdf 다운로드 API 흐름은 다음과 같습니다.
pdfUrl = null이라면, html을 pdf로 변환 -> s3에 pdf 업로드 -> pdfUrl을 테이블에 업데이트 -> pdfUrl 반환
pdfUrl != null이라면, pdfUrl 반환
이 형태라면 조회시에 pdf를 업로드 하는 로직이 포함될텐데, 그렇다면 최초 조회 성능이 약간 떨어질 것 같다는 단점이 있을 것 같긴 합니다!
There was a problem hiding this comment.
음 근데 생각해보니까 예약 수정 같은 api가 호출되면 그때마다 pdf를 갱신해줘야겠네요.. 그러면 pdfUrl이 null이 아니더라도 매번 pdf를 변환해서 s3에 업로드해야하는 이슈가 발생하겠네요..
그러면.. 예약 견적서 작성 또는 수정시에 pdf까지 변환해서 s3에 업로드를 해놔야할까요?? 살짝 헷갈리네요
수정시에는 기존 pdf url 기반으로 s3에 존재하던 기존 pdf 삭제 + 수정된 정보를 기반으로 새 pdf 업로드 후 db 업데이트
생성시에도 pdf 업로드
이런 흐름이 되어야하지 않을까 싶긴 하네요
There was a problem hiding this comment.
음 그렇다면 우선 현재는 pdf를 null로 두고 추후에 pdf 다운로드 api를 구현할때 s3 연동 작업 및 업로드 작업을 추가해도 괜찮을까요??
| private static void validateCreateReservation(User owner, User member, FoodTruck foodTruck) { | ||
| foodTruck.validateOwner(owner.getUserId()); | ||
| if (foodTruck.getOwner().getUserId().equals(member.getUserId())) { | ||
| throw new DomainRuleException(CANNOT_RESERVE_OWN_FOOD_TRUCK); | ||
| } | ||
| } |
There was a problem hiding this comment.
팩토리 메서드에서 사용하기 위한 검증 로직 좋네유 👍🏻
| public List<DateRange> getRanges() { | ||
| return Collections.unmodifiableList(ranges); | ||
| } |
#️⃣연관된 이슈
closes #25
📝작업 내용
기본 cru API이기 때문에 큰 문제는 없었습니다.
다만, 유효성 검증이 api 개발에 주요 고려사항이였습니다!
예약 견적서 작성
예약 조회
예약 수정
리팩토링 내용
reservationInfo.getFullAddress()메서드를 호출하여 전체 address를 반환하도록 수정했습니다.스크린샷 (선택)
💬리뷰 요구사항(선택)
ReservationDate 객체에서 fromJson이 잘 구현되어 있어서 손쉽게 구현할 수 있었습니다~ shout out to 퐁퐁균
카톡에서 말씀 드렸던 것처럼 dev.yml 파일은 개발 동안에는 ddl-auto: create으로 두도록 하겠습니다!
Summary by CodeRabbit