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
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ dependencies {
// Postgresql
runtimeOnly 'org.postgresql:postgresql'

// Postgis - Hibernate Spatial (JTS 타입을 DB Geometry 타입으로 매핑)
// Postgis - Hibernate Spatial (JTS 타입을 DB Geometry 타입으로 매핑)
implementation 'org.hibernate.orm:hibernate-spatial'
implementation 'org.geolatte:geolatte-geom:1.8.2'

Expand All @@ -55,6 +55,7 @@ dependencies {

// Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'

// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.14'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.mobility.api.domain.dispatch.controller;

// DispatchMatchingService 내부에서 TransporterService를 호출한다고 가정
// 혹은 여기서 바로 TransporterService를 호출해도 무방

import com.mobility.api.domain.transporter.dto.response.TransporterMatchResponse;
import com.mobility.api.domain.transporter.service.TransporterService;
import com.mobility.api.domain.dispatch.repository.DispatchRepository;
import com.mobility.api.global.exception.GlobalException;
import com.mobility.api.global.response.CommonResponse;
import com.mobility.api.global.response.ResultCode;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Tag(name = "Dispatch Matching", description = "배차 매칭 관련 API")
@RestController
@RequestMapping("/api/v1/dispatch")
@RequiredArgsConstructor
public class DispatchV1Controller {

private final DispatchRepository dispatchRepository;
private final TransporterService transporterService;

// 배차 등록 시, 주변의 기사 조회 (km 단위 반환)
@Operation(summary = "배차 주변 기사 추천", description = "해당 배차의 출발지 기준 반경 50km 이내 기사를 가까운 순으로 조회합니다.")
@GetMapping("/{dispatchId}/nearby-drivers")
public CommonResponse<List<TransporterMatchResponse>> getNearbyDrivers(@PathVariable Long dispatchId) {

// 1. 배차 정보 조회 (출발지 좌표 획득)
var dispatch = dispatchRepository.findById(dispatchId)
.orElseThrow(() -> new GlobalException(ResultCode.DISPATCH_NOT_FOUND));

// 2. 기사 검색 서비스 호출
List<TransporterMatchResponse> drivers = transporterService.findNearbyTransporters(
dispatch.getStartLatitude(),
dispatch.getStartLongitude()
);

return CommonResponse.success(drivers);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.mobility.api.domain.dispatch.dto.request;

import com.mobility.api.domain.dispatch.entity.Dispatch;
import com.mobility.api.domain.dispatch.enums.CallType;
import com.mobility.api.domain.dispatch.enums.ServiceType;
import com.mobility.api.domain.dispatch.enums.StatusType;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;

@Builder
public record DispatchReq(
@NotBlank(message = "출발지는 필수입니다.")
String startLocation,

@NotNull(message = "출발지 위도는 필수입니다.")
Double startLatitude,

@NotNull(message = "출발지 경도는 필수입니다.")
Double startLongitude,

@NotBlank(message = "도착지는 필수입니다.")
String destinationLocation,

@NotNull(message = "도착지 위도는 필수입니다.")
Double destinationLatitude,

@NotNull(message = "도착지 경도는 필수입니다.")
Double destinationLongitude,

@NotNull(message = "요금은 필수입니다.")
Integer charge,

@NotBlank(message = "고객 전화번호는 필수입니다.")
String clientPhoneNumber,

@NotNull(message = "콜 타입은 필수입니다.")
CallType callType,

@NotNull(message = "서비스 타입은 필수입니다.")
ServiceType serviceType,

Long officeId
) {
// DTO -> Entity 변환 메서드
public Dispatch toEntity() {
return Dispatch.builder()
.startLocation(startLocation)
.startLatitude(startLatitude)
.startLongitude(startLongitude)
.destinationLocation(destinationLocation)
.destinationLatitude(destinationLatitude)
.destinationLongitude(destinationLongitude)
.charge(charge)
.clientPhoneNumber(clientPhoneNumber)
.call(callType)
.service(serviceType)
.officeId(officeId)
.status(StatusType.OPEN) // 생성 시 기본 상태는 OPEN
.active(true) // 생성 시 기본 활성화
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.mobility.api.domain.dispatch.dto.response;

import com.mobility.api.domain.dispatch.entity.Dispatch;

// 배차 선택, 완료 res
public record DispatchAssignCompleteRes(
Long dispatcherId,
Long transporterId
) {
public static DispatchAssignCompleteRes from(Dispatch dispatch) {
// if (dispatch.getTransporter() != null) {
// throw new GlobalException(ResultCode.NOT_FOUND_USER);
// }

return new DispatchAssignCompleteRes(
dispatch.getId(),
dispatch.getTransporter().getId()
);
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,60 @@
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.ServiceType;
import com.mobility.api.domain.dispatch.enums.StatusType;
import lombok.Builder;

// 배차 선택, 완료 res
public record DispatchRes(
Long dispatcherId,
Long transporterId
import java.time.LocalDateTime;

/**
* 배차 등록 res
*/

@Builder
public record DispatchRes(Long id,
String startLocation,
Double startLatitude,
Double startLongitude,
String destinationLocation,
Double destinationLatitude,
Double destinationLongitude,
Integer charge,
String clientPhoneNumber,
StatusType status,
CallType callType,
ServiceType serviceType,
Long officeId,
Long transporterId, // 기사 ID (null 가능)
String transporterName, // 기사 이름 (null 가능)
LocalDateTime createdAt
) {
// Entity -> DTO 변환 메서드 (거리는 별도 계산 없을 시 null)
public static DispatchRes from(Dispatch dispatch) {
// if (dispatch.getTransporter() != null) {
// throw new GlobalException(ResultCode.NOT_FOUND_USER);
// }
return from(dispatch, null);
}

return new DispatchRes(
dispatch.getId(),
dispatch.getTransporter().getId()
);
// Entity + 거리 -> DTO 변환 메서드
public static DispatchRes from(Dispatch dispatch, Double distance) {
return DispatchRes.builder()
.id(dispatch.getId())
.startLocation(dispatch.getStartLocation())
.startLatitude(dispatch.getStartLatitude())
.startLongitude(dispatch.getStartLongitude())
.destinationLocation(dispatch.getDestinationLocation())
.destinationLatitude(dispatch.getDestinationLatitude())
.destinationLongitude(dispatch.getDestinationLongitude())
.charge(dispatch.getCharge())
.clientPhoneNumber(dispatch.getClientPhoneNumber())
.status(dispatch.getStatus())
.callType(dispatch.getCall())
.serviceType(dispatch.getService())
.officeId(dispatch.getOfficeId())
// Transporter가 lazy loading이거나 null일 수 있으므로 체크 필요
.transporterId(dispatch.getTransporter() != null ? dispatch.getTransporter().getId() : null)
.transporterName(dispatch.getTransporter() != null ? dispatch.getTransporter().getName() : null)
.createdAt(dispatch.getCreatedAt())
.build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@ public class Dispatch {
private Long id;

private String startLocation; // 출발지

private double startLatitude; // 출발지 위도
private double startLongitude; // 출발지 경도

private String destinationLocation; // 도착지

private double destinationLatitude; // 도착지 위도
private double destinationLongitude; // 도착지 경도

private Integer charge; // 요금
private String clientPhoneNumber; // 고객 전화번호

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import com.mobility.api.domain.dispatch.dto.response.DispatchCancelRes;
import com.mobility.api.domain.dispatch.entity.Dispatch;
import com.mobility.api.domain.dispatch.repository.DispatchRepository;
import com.mobility.api.domain.dispatch.dto.response.DispatchRes;
import com.mobility.api.domain.dispatch.dto.response.DispatchAssignCompleteRes;
import com.mobility.api.domain.transporter.entity.Transporter;
import com.mobility.api.domain.transporter.repository.TransporterRepository;
import com.mobility.api.global.exception.GlobalException;
Expand All @@ -22,7 +22,7 @@ public class DispatcherService {
private final TransporterRepository transporterRepository;

@Transactional
public DispatchRes assignDispatch(Long dispatchId, Long transporterId) {
public DispatchAssignCompleteRes assignDispatch(Long dispatchId, Long transporterId) {

// 1. 기사 정보 조회
Transporter transporter = transporterRepository.findById(transporterId)
Expand All @@ -35,7 +35,7 @@ public DispatchRes assignDispatch(Long dispatchId, Long transporterId) {
// 3. 배차 할당
dispatch.assignDispatch(transporter);

return DispatchRes.from(dispatch);
return DispatchAssignCompleteRes.from(dispatch);
}

@Transactional
Expand All @@ -55,7 +55,7 @@ public DispatchCancelRes cancelDispatch(Long dispatchId, Long transporterId) {
}

@Transactional
public DispatchRes completeDispatch(Long dispatchId, Long transporterId) {
public DispatchAssignCompleteRes completeDispatch(Long dispatchId, Long transporterId) {

// 1. 기사 정보 조회
Transporter transporter = transporterRepository.findById(transporterId)
Expand All @@ -67,6 +67,6 @@ public DispatchRes completeDispatch(Long dispatchId, Long transporterId) {

dispatch.completeDispatch(transporter);

return DispatchRes.from(dispatch);
return DispatchAssignCompleteRes.from(dispatch);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.mobility.api.domain.transporter.controller;

import com.mobility.api.domain.dispatch.dto.response.DispatchCancelRes;
import com.mobility.api.domain.dispatch.dto.response.DispatchRes;
import com.mobility.api.domain.dispatch.dto.response.DispatchAssignCompleteRes;
import com.mobility.api.domain.dispatch.service.DispatcherService;
import com.mobility.api.domain.transporter.dto.request.LocationUpdateReq;
import com.mobility.api.domain.transporter.dto.response.LocationUpdateRes;
Expand All @@ -16,7 +16,6 @@
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@Slf4j
Expand All @@ -31,7 +30,7 @@ public class TransporterV1Controller {

@Operation(summary = "배차 할당", description = "")
@PatchMapping("/dispatch-assign/{dispatchId}")
public CommonResponse<DispatchRes> assignDispatch(
public CommonResponse<DispatchAssignCompleteRes> assignDispatch(
@PathVariable Long dispatchId, @CurrentUser Transporter transporter) {

Long transporterId = getValidatedTransporterId(transporter);
Expand All @@ -51,7 +50,7 @@ public CommonResponse<DispatchCancelRes> cancelDispatch(

@Operation(summary = "배차 완료", description = "")
@PatchMapping("/dispatch-complete/{dispatchId}")
public CommonResponse<DispatchRes> completeDispatch(
public CommonResponse<DispatchAssignCompleteRes> completeDispatch(
@PathVariable Long dispatchId, @CurrentUser Transporter transporter) {

Long transporterId = getValidatedTransporterId(transporter);
Expand All @@ -68,7 +67,7 @@ private Long getValidatedTransporterId(Transporter transporter) {
return transporterId;
}

/*
/**
** 기사 위치 정보 수집 관련
*/
@Operation(summary = "기사 실시간 위치 업데이트", description = "기사 앱에서 전송된 위도(latitude), 경도(longitude) 정보를 저장")
Expand All @@ -83,4 +82,8 @@ public CommonResponse<LocationUpdateRes> updateTransporterLocation(

return CommonResponse.success(response);
}

/**
* 기사의 배차 리스트 정렬 등을 위해, 현재 기사 위치 기준으로 배차
*/
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.mobility.api.domain.transporter.dto;

/**
* Native Query 결과를 매핑할 인터페이스
*/

public interface TransporterDistanceProjection {
Long getId();
String getName();
String getPhone();
Double getDistanceInMeters(); // DB에서 계산된 미터 단위 거리
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.mobility.api.domain.transporter.dto.response;

import com.mobility.api.domain.transporter.dto.TransporterDistanceProjection;
import lombok.Builder;

/**
* Projection의 미터 값을 받아 km로 변환하여 프론트에 전달
*/

@Builder
public record TransporterMatchResponse(
Long transporterId,
String name,
String phone,
Double distanceKm // km 단위
) {
public static TransporterMatchResponse from(TransporterDistanceProjection proj) {
// 미터(m) -> 킬로미터(km) 변환
// 값이 없으면 0.0 처리
double meters = proj.getDistanceInMeters() != null ? proj.getDistanceInMeters() : 0.0;

// 1000으로 나누고, 소수점 둘째 자리까지 반올림 (예: 1.25 km)
double km = Math.round((meters / 1000.0) * 100.0) / 100.0;

return TransporterMatchResponse.builder()
.transporterId(proj.getId())
.name(proj.getName())
.phone(proj.getPhone())
.distanceKm(km)
.build();
}
}
Loading