Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,12 @@ List<DispatchDistanceProjection> findDispatchesByDistance(
*/
@Query("SELECT d FROM Dispatch d LEFT JOIN FETCH d.transporter")
Page<Dispatch> findAllWithTransporter(Pageable pageable);

/**
* 특정 기사의 특정 상태 배차 조회
* @param transporterId 기사 ID
* @param status 배차 상태
* @return 배차 정보
*/
Optional<Dispatch> findByTransporterIdAndStatus(Long transporterId, StatusType status);
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
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;
import com.mobility.api.domain.dispatch.entity.Dispatch;
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;
Expand Down Expand Up @@ -45,6 +47,9 @@ public DispatchAssignCompleteRes assignDispatch(Long dispatchId, Long transporte
// 3. 배차 할당
dispatch.assignDispatch(transporter);

// 4. 기사의 배차 상태를 DISPATCH로 변경 (배차중인 오더가 있음)
transporter.changeDispatchStatus(DispatchStatus.DISPATCH);

return DispatchAssignCompleteRes.from(dispatch);
}

Expand All @@ -61,6 +66,9 @@ public DispatchCancelRes cancelDispatch(Long dispatchId, Long transporterId) {

dispatch.cancelDispatch(transporter);

// 3. 기사의 배차 상태를 EMPTY로 변경 (배차중인 오더가 없음)
transporter.changeDispatchStatus(DispatchStatus.EMPTY);

return DispatchCancelRes.from(dispatch);
}

Expand All @@ -77,6 +85,9 @@ public DispatchAssignCompleteRes completeDispatch(Long dispatchId, Long transpor

dispatch.completeDispatch(transporter);

// 3. 기사의 배차 상태를 EMPTY로 변경 (배차중인 오더가 없음)
transporter.changeDispatchStatus(DispatchStatus.EMPTY);

return DispatchAssignCompleteRes.from(dispatch);
}

Expand Down Expand Up @@ -118,11 +129,20 @@ public DispatchDetailRes getDispatchDetail(Long dispatchId, Long currentUserId)
* @return 거리순으로 정렬된 배차 리스트
*/
public List<DispatchListItemRes> getDispatchListByDistance(Long transporterId, List<StatusType> 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();

Expand All @@ -137,12 +157,35 @@ public List<DispatchListItemRes> getDispatchListByDistance(Long transporterId, L

List<DispatchDistanceProjection> projections = dispatchRepository.findDispatchesByDistance(lat, lon, statusStrings);

// 3. Projection -> DTO 변환
// 4. Projection -> DTO 변환
return projections.stream()
.map(DispatchListItemRes::from)
.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 출발지 위도
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.mobility.api.domain.transporter;

public enum DispatchStatus {
EMPTY, // 배차중인 오더가 없는 상태
DISPATCH // 배차중인 오더가 있는 상태
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -87,6 +88,39 @@ public CommonResponse<LocationUpdateRes> 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<CurrentDispatchDetailRes> 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);
}

/**
* 기사용 배차 리스트 조회 (거리순 정렬 + 상태 필터링)
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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번대 (사무실 관련)
Expand Down