From 1b9eafe99aa306ce1869c90a3dae32694646e6a4 Mon Sep 17 00:00:00 2001 From: SOWON LEE <66356241+Leesowon@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:27:02 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[#59]=20feat:=20=EA=B8=B0=EC=82=AC=20?= =?UTF-8?q?=EB=B0=B0=EC=B0=A8=20=EC=83=81=ED=83=9C=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DispatchStatus enum 추가 (EMPTY, DISPATCH) - Transporter 엔티티에 dispatchStatus 필드 추가 - 배차중인 기사는 배차 리스트 조회 불가 처리 - 배차 수락 시 상태를 DISPATCH로 변경 - 배차 취소/완료 시 상태를 EMPTY로 변경 - TRANSPORTER_ALREADY_DISPATCHED 에러 코드 추가 (3004) --- .../dispatch/service/DispatcherService.java | 25 ++++++++++++++++--- .../domain/transporter/DispatchStatus.java | 6 +++++ .../transporter/entity/Transporter.java | 11 ++++++++ .../api/global/response/ResultCode.java | 1 + 4 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/mobility/api/domain/transporter/DispatchStatus.java diff --git a/src/main/java/com/mobility/api/domain/dispatch/service/DispatcherService.java b/src/main/java/com/mobility/api/domain/dispatch/service/DispatcherService.java index ce54b1a..f70fb8c 100644 --- a/src/main/java/com/mobility/api/domain/dispatch/service/DispatcherService.java +++ b/src/main/java/com/mobility/api/domain/dispatch/service/DispatcherService.java @@ -8,6 +8,7 @@ import com.mobility.api.domain.dispatch.enums.StatusType; import com.mobility.api.domain.dispatch.repository.DispatchRepository; import com.mobility.api.domain.dispatch.dto.response.DispatchAssignCompleteRes; +import com.mobility.api.domain.transporter.DispatchStatus; import com.mobility.api.domain.transporter.entity.LocationHistory; import com.mobility.api.domain.transporter.entity.Transporter; import com.mobility.api.domain.transporter.repository.LocationRepository; @@ -45,6 +46,9 @@ public DispatchAssignCompleteRes assignDispatch(Long dispatchId, Long transporte // 3. 배차 할당 dispatch.assignDispatch(transporter); + // 4. 기사의 배차 상태를 DISPATCH로 변경 (배차중인 오더가 있음) + transporter.changeDispatchStatus(DispatchStatus.DISPATCH); + return DispatchAssignCompleteRes.from(dispatch); } @@ -61,6 +65,9 @@ public DispatchCancelRes cancelDispatch(Long dispatchId, Long transporterId) { dispatch.cancelDispatch(transporter); + // 3. 기사의 배차 상태를 EMPTY로 변경 (배차중인 오더가 없음) + transporter.changeDispatchStatus(DispatchStatus.EMPTY); + return DispatchCancelRes.from(dispatch); } @@ -77,6 +84,9 @@ public DispatchAssignCompleteRes completeDispatch(Long dispatchId, Long transpor dispatch.completeDispatch(transporter); + // 3. 기사의 배차 상태를 EMPTY로 변경 (배차중인 오더가 없음) + transporter.changeDispatchStatus(DispatchStatus.EMPTY); + return DispatchAssignCompleteRes.from(dispatch); } @@ -118,11 +128,20 @@ public DispatchDetailRes getDispatchDetail(Long dispatchId, Long currentUserId) * @return 거리순으로 정렬된 배차 리스트 */ public List getDispatchListByDistance(Long transporterId, List statuses) { - // 1. 기사의 최신 위치 조회 + // 1. 기사 정보 조회 및 배차 상태 체크 + Transporter transporter = transporterRepository.findById(transporterId) + .orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_USER)); + + // 배차 상태가 DISPATCH인 경우 (이미 배차중인 오더가 있는 경우) 에러 + if (transporter.getDispatchStatus() == DispatchStatus.DISPATCH) { + throw new GlobalException(ResultCode.TRANSPORTER_ALREADY_DISPATCHED); + } + + // 2. 기사의 최신 위치 조회 LocationHistory latestLocation = locationRepository.findFirstByTransporter_IdOrderByIdDesc(transporterId) .orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_USER)); - // 2. 기사 위치 기준으로 배차를 거리순으로 조회 (상태 필터링 적용) + // 3. 기사 위치 기준으로 배차를 거리순으로 조회 (상태 필터링 적용) double lat = latestLocation.getLocation().getY(); double lon = latestLocation.getLocation().getX(); @@ -137,7 +156,7 @@ public List getDispatchListByDistance(Long transporterId, L List projections = dispatchRepository.findDispatchesByDistance(lat, lon, statusStrings); - // 3. Projection -> DTO 변환 + // 4. Projection -> DTO 변환 return projections.stream() .map(DispatchListItemRes::from) .collect(Collectors.toList()); diff --git a/src/main/java/com/mobility/api/domain/transporter/DispatchStatus.java b/src/main/java/com/mobility/api/domain/transporter/DispatchStatus.java new file mode 100644 index 0000000..9e6b28d --- /dev/null +++ b/src/main/java/com/mobility/api/domain/transporter/DispatchStatus.java @@ -0,0 +1,6 @@ +package com.mobility.api.domain.transporter; + +public enum DispatchStatus { + EMPTY, // 배차중인 오더가 없는 상태 + DISPATCH // 배차중인 오더가 있는 상태 +} diff --git a/src/main/java/com/mobility/api/domain/transporter/entity/Transporter.java b/src/main/java/com/mobility/api/domain/transporter/entity/Transporter.java index fa0903f..325ca6a 100644 --- a/src/main/java/com/mobility/api/domain/transporter/entity/Transporter.java +++ b/src/main/java/com/mobility/api/domain/transporter/entity/Transporter.java @@ -3,6 +3,7 @@ import com.mobility.api.domain.office.entity.Office; import com.mobility.api.global.entity.BaseEntity; import com.mobility.api.domain.transporter.TransporterStatus; +import com.mobility.api.domain.transporter.DispatchStatus; import jakarta.persistence.*; import lombok.*; import lombok.experimental.SuperBuilder; @@ -41,6 +42,11 @@ public class Transporter extends BaseEntity { @Enumerated(EnumType.STRING) private TransporterStatus status = TransporterStatus.PENDING; + // 배차 상태 필드 (기본값: EMPTY - 배차중인 오더가 없는 상태) + @Enumerated(EnumType.STRING) + @Column(name = "dispatch_status") + private DispatchStatus dispatchStatus = DispatchStatus.EMPTY; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "office_id") // DB 컬럼명: office_id private Office office; @@ -49,4 +55,9 @@ public class Transporter extends BaseEntity { public void changeStatus(TransporterStatus newStatus) { this.status = newStatus; } + + // 배차 상태 변경 편의 메서드 (Dirty Checking용) + public void changeDispatchStatus(DispatchStatus newDispatchStatus) { + this.dispatchStatus = newDispatchStatus; + } } diff --git a/src/main/java/com/mobility/api/global/response/ResultCode.java b/src/main/java/com/mobility/api/global/response/ResultCode.java index 7cf1cf3..e759e38 100644 --- a/src/main/java/com/mobility/api/global/response/ResultCode.java +++ b/src/main/java/com/mobility/api/global/response/ResultCode.java @@ -37,6 +37,7 @@ public enum ResultCode { TRANSPORTER_LOCATION_SAVE_SUCCESS(HttpStatus.OK, 3001, "기사 위도 경도 정보가 저장되었습니다"), NOT_FOUND_TRANSPORTER(HttpStatus.NOT_FOUND, 3002, "기사 정보를 찾을 수 없습니다."), UNAUTHORIZED_ACCESS(HttpStatus.NOT_FOUND, 3003, "해당 기사 수정 권한이 없습니다."), + TRANSPORTER_ALREADY_DISPATCHED(HttpStatus.CONFLICT, 3004, "이미 배차중인 오더가 있습니다."), /** * 4000번대 (사무실 관련) From a3419a7493ae58af4b4008b43624e9b7a13c4114 Mon Sep 17 00:00:00 2001 From: SOWON LEE <66356241+Leesowon@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:40:43 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[#59]=20feat:=20=ED=98=84=EC=9E=AC=20?= =?UTF-8?q?=EB=B0=B0=EC=B0=A8=EC=A4=91=EC=9D=B8=20=EC=98=A4=EB=8D=94=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/CurrentDispatchDetailRes.java | 89 +++++++++++++++++++ .../repository/DispatchRepository.java | 8 ++ .../dispatch/service/DispatcherService.java | 24 +++++ .../controller/TransporterV1Controller.java | 34 +++++++ 4 files changed, 155 insertions(+) create mode 100644 src/main/java/com/mobility/api/domain/dispatch/dto/response/CurrentDispatchDetailRes.java diff --git a/src/main/java/com/mobility/api/domain/dispatch/dto/response/CurrentDispatchDetailRes.java b/src/main/java/com/mobility/api/domain/dispatch/dto/response/CurrentDispatchDetailRes.java new file mode 100644 index 0000000..ea74c74 --- /dev/null +++ b/src/main/java/com/mobility/api/domain/dispatch/dto/response/CurrentDispatchDetailRes.java @@ -0,0 +1,89 @@ +package com.mobility.api.domain.dispatch.dto.response; + +import com.mobility.api.domain.dispatch.entity.Dispatch; +import com.mobility.api.domain.dispatch.enums.CallType; +import com.mobility.api.domain.dispatch.enums.PaymentType; +import com.mobility.api.domain.dispatch.enums.ServiceType; +import com.mobility.api.domain.dispatch.enums.StatusType; +import com.mobility.api.domain.dispatch.enums.TollType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +import java.time.LocalDateTime; + +/** + * 현재 배차중인 오더 상세 정보 조회 응답 DTO + */ +@Builder +@Schema(description = "현재 배차중인 오더 상세 정보 응답") +public record CurrentDispatchDetailRes( + @Schema(description = "배차 ID", example = "1") + Long id, + + @Schema(description = "배차 상태", example = "ASSIGNED") + StatusType status, + + @Schema(description = "요금 (원)", example = "150000") + Integer charge, + + @Schema(description = "출발지", example = "서울특별시 강남구 테헤란로 123") + String startLocation, + + @Schema(description = "도착지", example = "부산광역시 해운대구 우동 456") + String destinationLocation, + + @Schema(description = "고객 전화번호", example = "010-****-5678") + String clientPhoneNumber, + + @Schema(description = "메모", example = "학교 비밀번호 1234") + String memo, + + @Schema(description = "콜 타입 (INTERNAL: 자사콜, INTEGRATED: 통합콜)", example = "INTERNAL") + CallType call, + + @Schema(description = "서비스 타입 (DELIVERY: 탁송, DRIVER: 대리)", example = "DELIVERY") + ServiceType service, + + @Schema(description = "결제 방식 (CASH: 현금, POSTPAID: 후불, COMPLETE_POSTPAID: 완후)", example = "CASH") + PaymentType paymentMethod, + + @Schema(description = "톨비 방식 (TOLLGATE_INCLUDED: 톨게이트 포함, TOLLGATE_SEPARATE: 톨게이트 별도, HIPASS: 하이패스)", example = "HIPASS") + TollType tollType, + + @Schema(description = "사무실 ID", example = "1") + Long officeId, + + @Schema(description = "생성일시", example = "2024-01-15T10:00:00") + LocalDateTime createdAt, + + @Schema(description = "수정일시", example = "2024-01-15T10:00:00") + LocalDateTime updatedAt, + + @Schema(description = "배차 할당 시간", example = "2024-01-15T10:05:00") + LocalDateTime assignedAt +) { + /** + * Entity -> DTO 변환 메서드 + * @param dispatch 배차 엔티티 + * @return CurrentDispatchDetailRes + */ + public static CurrentDispatchDetailRes from(Dispatch dispatch) { + return CurrentDispatchDetailRes.builder() + .id(dispatch.getId()) + .status(dispatch.getStatus()) + .charge(dispatch.getCharge()) + .startLocation(dispatch.getStartLocation()) + .destinationLocation(dispatch.getDestinationLocation()) + .clientPhoneNumber(dispatch.getClientPhoneNumber()) + .memo(dispatch.getMemo()) + .call(dispatch.getCall()) + .service(dispatch.getService()) + .paymentMethod(dispatch.getPaymentType()) + .tollType(dispatch.getTollType()) + .officeId(dispatch.getOfficeId()) + .createdAt(dispatch.getCreatedAt()) + .updatedAt(dispatch.getUpdatedAt()) + .assignedAt(dispatch.getAssignedAt()) + .build(); + } +} diff --git a/src/main/java/com/mobility/api/domain/dispatch/repository/DispatchRepository.java b/src/main/java/com/mobility/api/domain/dispatch/repository/DispatchRepository.java index 91d4f80..568734a 100644 --- a/src/main/java/com/mobility/api/domain/dispatch/repository/DispatchRepository.java +++ b/src/main/java/com/mobility/api/domain/dispatch/repository/DispatchRepository.java @@ -74,4 +74,12 @@ List findDispatchesByDistance( */ @Query("SELECT d FROM Dispatch d LEFT JOIN FETCH d.transporter") Page findAllWithTransporter(Pageable pageable); + + /** + * 특정 기사의 특정 상태 배차 조회 + * @param transporterId 기사 ID + * @param status 배차 상태 + * @return 배차 정보 + */ + Optional findByTransporterIdAndStatus(Long transporterId, StatusType status); } diff --git a/src/main/java/com/mobility/api/domain/dispatch/service/DispatcherService.java b/src/main/java/com/mobility/api/domain/dispatch/service/DispatcherService.java index f70fb8c..907d05a 100644 --- a/src/main/java/com/mobility/api/domain/dispatch/service/DispatcherService.java +++ b/src/main/java/com/mobility/api/domain/dispatch/service/DispatcherService.java @@ -1,6 +1,7 @@ package com.mobility.api.domain.dispatch.service; import com.mobility.api.domain.dispatch.dto.DispatchDistanceProjection; +import com.mobility.api.domain.dispatch.dto.response.CurrentDispatchDetailRes; import com.mobility.api.domain.dispatch.dto.response.DispatchCancelRes; import com.mobility.api.domain.dispatch.dto.response.DispatchDetailRes; import com.mobility.api.domain.dispatch.dto.response.DispatchListItemRes; @@ -162,6 +163,29 @@ public List getDispatchListByDistance(Long transporterId, L .collect(Collectors.toList()); } + /** + * 현재 배차중인 오더 상세 정보 조회 + * @param transporterId 기사 ID + * @return CurrentDispatchDetailRes 현재 배차중인 오더 상세 정보 + */ + public CurrentDispatchDetailRes getCurrentDispatch(Long transporterId) { + // 1. 기사 정보 조회 + Transporter transporter = transporterRepository.findById(transporterId) + .orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_USER)); + + // 2. 배차 상태가 EMPTY인 경우 (배차중인 오더가 없는 경우) 에러 + if (transporter.getDispatchStatus() == DispatchStatus.EMPTY) { + throw new GlobalException(ResultCode.DISPATCH_NOT_ASSIGNED); + } + + // 3. 기사에게 ASSIGNED 상태로 배차된 오더 조회 + Dispatch dispatch = dispatchRepository.findByTransporterIdAndStatus(transporterId, StatusType.ASSIGNED) + .orElseThrow(() -> new GlobalException(ResultCode.DISPATCH_NOT_ASSIGNED)); + + // 4. DTO 변환 및 반환 + return CurrentDispatchDetailRes.from(dispatch); + } + /** * PostGIS를 사용하여 두 지점 간 거리 계산 (km 단위) * @param startLat 출발지 위도 diff --git a/src/main/java/com/mobility/api/domain/transporter/controller/TransporterV1Controller.java b/src/main/java/com/mobility/api/domain/transporter/controller/TransporterV1Controller.java index 9dd6228..64c1d47 100644 --- a/src/main/java/com/mobility/api/domain/transporter/controller/TransporterV1Controller.java +++ b/src/main/java/com/mobility/api/domain/transporter/controller/TransporterV1Controller.java @@ -1,5 +1,6 @@ package com.mobility.api.domain.transporter.controller; +import com.mobility.api.domain.dispatch.dto.response.CurrentDispatchDetailRes; import com.mobility.api.domain.dispatch.dto.response.DispatchCancelRes; import com.mobility.api.domain.dispatch.dto.response.DispatchAssignCompleteRes; import com.mobility.api.domain.dispatch.dto.response.DispatchListItemRes; @@ -87,6 +88,39 @@ public CommonResponse updateTransporterLocation( return CommonResponse.success(response); } + /** + * 현재 배차중인 오더 상세 정보 조회 + */ + @Operation( + summary = "현재 배차중인 오더 상세 정보 조회", + description = """ + 현재 로그인한 기사가 배차중인 오더의 상세 정보를 조회합니다. + + - 기사의 dispatchStatus가 DISPATCH 상태일 때만 조회 가능합니다. + - EMPTY 상태(배차중인 오더가 없음)인 경우 에러가 반환됩니다. + - ASSIGNED 상태의 배차 정보를 반환합니다. + """ + ) + @io.swagger.v3.oas.annotations.responses.ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "현재 배차중인 오더 상세 정보 조회 성공" + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "404", + description = "배차중인 오더가 없음 (DISPATCH_NOT_ASSIGNED)" + ) + }) + @GetMapping("/current-dispatch") + public CommonResponse getCurrentDispatch( + @io.swagger.v3.oas.annotations.Parameter(hidden = true) + @CurrentUser Transporter transporter + ) { + Long transporterId = getValidatedTransporterId(transporter); + CurrentDispatchDetailRes currentDispatch = dispatcherService.getCurrentDispatch(transporterId); + return CommonResponse.success(currentDispatch); + } + /** * 기사용 배차 리스트 조회 (거리순 정렬 + 상태 필터링) *