diff --git a/src/main/java/com/mobility/api/MobilityApiApplication.java b/src/main/java/com/mobility/api/MobilityApiApplication.java index 753f121..cfbf1cb 100644 --- a/src/main/java/com/mobility/api/MobilityApiApplication.java +++ b/src/main/java/com/mobility/api/MobilityApiApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication +@EnableJpaAuditing public class MobilityApiApplication { public static void main(String[] args) { diff --git a/src/main/java/com/mobility/api/domain/dispatch/entity/Dispatch.java b/src/main/java/com/mobility/api/domain/dispatch/entity/Dispatch.java index ef84e7d..88a0a55 100644 --- a/src/main/java/com/mobility/api/domain/dispatch/entity/Dispatch.java +++ b/src/main/java/com/mobility/api/domain/dispatch/entity/Dispatch.java @@ -2,10 +2,12 @@ import com.mobility.api.domain.dispatch.enums.*; import com.mobility.api.domain.transporter.entity.Transporter; +import com.mobility.api.global.entity.BaseEntity; import com.mobility.api.global.exception.GlobalException; import com.mobility.api.global.response.ResultCode; import jakarta.persistence.*; import lombok.*; +import lombok.experimental.SuperBuilder; import lombok.extern.slf4j.Slf4j; import java.time.LocalDateTime; @@ -15,14 +17,16 @@ @Setter @NoArgsConstructor @AllArgsConstructor -@Builder +@SuperBuilder @Slf4j -public class Dispatch { +public class Dispatch extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + private String dispatchNumber; // 배차 번호 (예: 2024-0001) + private String startLocation; // 출발지 private double startLatitude; // 출발지 위도 @@ -67,18 +71,28 @@ public class Dispatch { @JoinColumn(name = "transporter_id") private Transporter transporter; - private LocalDateTime createdAt; // 생성일자 + private String memo; // 메모 + + private LocalDateTime assignedAt; // 배차 할당 시간 + + private LocalDateTime completedAt; // 완료 시간 + + private LocalDateTime canceledAt; // 취소 시간 + + @Column(length = 500) + private String cancelReason; // 취소 사유 (최대 200자) // 기사 배차 시 public void assignDispatch(Transporter transporter) { - // 1. 유효성 검증 : 이미 배차되어있는지 확인 - if (this.status != StatusType.OPEN) { + // 1. 유효성 검증 : HOLD 또는 OPEN 상태에서만 배차 가능 + if (this.status != StatusType.OPEN && this.status != StatusType.HOLD) { throw new GlobalException(ResultCode.DISPATCH_NOT_OPEN); } this.transporter = transporter; this.status = StatusType.ASSIGNED; + this.assignedAt = LocalDateTime.now(); } public void cancelDispatch(Transporter transporter) { @@ -100,6 +114,7 @@ public void completeDispatch(Transporter transporter) { } this.status = StatusType.COMPLETED; + this.completedAt = LocalDateTime.now(); } private void validateOwner(Transporter transporter) { diff --git a/src/main/java/com/mobility/api/domain/dispatch/enums/StatusType.java b/src/main/java/com/mobility/api/domain/dispatch/enums/StatusType.java index 6166587..b8d07a9 100644 --- a/src/main/java/com/mobility/api/domain/dispatch/enums/StatusType.java +++ b/src/main/java/com/mobility/api/domain/dispatch/enums/StatusType.java @@ -1,8 +1,9 @@ package com.mobility.api.domain.dispatch.enums; public enum StatusType { - OPEN, // 등록 - ASSIGNED, // 진행중 - COMPLETED, // 완료 + HOLD, // 자동배차 진행중 (주변 기사에게 순차 권유 중) + OPEN, // 모든 기사 확인 가능 (자동배차 대상 없거나 모두 거절) + ASSIGNED, // 배차 완료 (기사 배정됨) + COMPLETED, // 운송 완료 CANCELED // 취소 } 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 1edd194..4d2c241 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 @@ -7,6 +7,8 @@ import org.springframework.data.jpa.repository.*; import org.springframework.data.repository.query.Param; +import com.mobility.api.domain.dispatch.enums.StatusType; + import java.util.List; import java.util.Optional; @@ -50,4 +52,16 @@ List findDispatchesByDistance( @Param("lon") double lon, @Param("statuses") List statuses ); + + /** + * 상태별 배차 카운트 조회 + */ + @Query("SELECT d.status, COUNT(d) FROM Dispatch d GROUP BY d.status") + List countByStatus(); + + /** + * 특정 사무실의 상태별 배차 카운트 조회 + */ + @Query("SELECT d.status, COUNT(d) FROM Dispatch d WHERE d.officeId = :officeId GROUP BY d.status") + List countByStatusAndOfficeId(@Param("officeId") Long officeId); } diff --git a/src/main/java/com/mobility/api/domain/dispatch/service/AutoDispatchService.java b/src/main/java/com/mobility/api/domain/dispatch/service/AutoDispatchService.java index 6a6b81d..7db8444 100644 --- a/src/main/java/com/mobility/api/domain/dispatch/service/AutoDispatchService.java +++ b/src/main/java/com/mobility/api/domain/dispatch/service/AutoDispatchService.java @@ -85,8 +85,8 @@ public void startSequentialNotification(Long dispatchId) { dispatch = dispatchRepository.findById(dispatchId) .orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_DISPATCH)); - if (dispatch.getStatus() != StatusType.OPEN) { - log.info("[AutoDispatch] 배차가 더 이상 OPEN 상태가 아님. 종료 - dispatchId: {}, status: {}", + if (dispatch.getStatus() != StatusType.HOLD) { + log.info("[AutoDispatch] 배차가 더 이상 HOLD 상태가 아님. 종료 - dispatchId: {}, status: {}", dispatchId, dispatch.getStatus()); break; } @@ -124,8 +124,9 @@ public void startSequentialNotification(Long dispatchId) { } } - // 4. 10명 모두 거절/미응답 - log.info("[AutoDispatch] 모든 기사가 거절/미응답. 배차는 OPEN 상태 유지 - dispatchId: {}", dispatchId); + // 4. 모든 기사가 거절/미응답 → HOLD에서 OPEN으로 변경 + updateDispatchStatusToOpen(dispatchId); + log.info("[AutoDispatch] 모든 기사가 거절/미응답. 배차를 OPEN 상태로 변경 - dispatchId: {}", dispatchId); } catch (Exception e) { log.error("[AutoDispatch] 순차 알림 처리 중 오류 발생 - dispatchId: {}", dispatchId, e); @@ -160,8 +161,8 @@ public void handleAccept(Long offerId, Long transporterId) { Dispatch dispatch = dispatchRepository.findByIdWithPessimisticLock(offer.getDispatch().getId()) .orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_DISPATCH)); - // 5. 배차 상태 확인 (이미 할당되었을 수 있음) - if (dispatch.getStatus() != StatusType.OPEN) { + // 5. 배차 상태 확인 (HOLD 상태에서만 수락 가능) + if (dispatch.getStatus() != StatusType.HOLD) { // Offer는 거절로 처리 offer.reject(); offerRepository.save(offer); @@ -352,4 +353,18 @@ private Double calculateDistance(double startLat, double startLon, double endLat return earthRadiusKm * c; } + + /** + * 배차 상태를 OPEN으로 변경 (모든 자동배차 대상 기사가 거절/타임아웃한 경우) + */ + @Transactional + protected void updateDispatchStatusToOpen(Long dispatchId) { + dispatchRepository.findById(dispatchId).ifPresent(dispatch -> { + if (dispatch.getStatus() == StatusType.HOLD) { + dispatch.setStatus(StatusType.OPEN); + dispatchRepository.save(dispatch); + log.info("[AutoDispatch] 배차 상태 HOLD → OPEN 변경 완료 - dispatchId: {}", dispatchId); + } + }); + } } diff --git a/src/main/java/com/mobility/api/domain/office/controller/OfficeV1Controller.java b/src/main/java/com/mobility/api/domain/office/controller/OfficeV1Controller.java index c19b8c7..e05daaf 100644 --- a/src/main/java/com/mobility/api/domain/office/controller/OfficeV1Controller.java +++ b/src/main/java/com/mobility/api/domain/office/controller/OfficeV1Controller.java @@ -4,7 +4,9 @@ import com.mobility.api.domain.office.dto.request.CreateDispatchReq; import com.mobility.api.domain.office.dto.request.DispatchSearchDto; import com.mobility.api.domain.office.dto.request.UpdateDispatchReq; +import com.mobility.api.domain.office.dto.response.DispatchSummaryRes; import com.mobility.api.domain.office.dto.response.GetAllDispatchRes; +import com.mobility.api.domain.office.dto.response.GetDispatchDetailRes; import com.mobility.api.domain.office.service.OfficeService; import com.mobility.api.domain.transporter.dto.request.TransporterCreateReq; import com.mobility.api.domain.transporter.dto.response.TransporterRes; @@ -50,6 +52,35 @@ public CommonResponse> getAllDispatch( return CommonResponse.success(officeService.findAllDispatch(searchDto, pageable)); } + /** + *
+     *     사무실 - 배차 상세 조회
+     * 
+ * + * @param dispatchId + * @return + */ + @Operation(summary = "배차 상세 조회", description = "") + @RequestMapping(path = "/dispatch/{dispatch_id}", method = RequestMethod.GET) + public CommonResponse getDispatchDetail( + @PathVariable("dispatch_id") Long dispatchId + ) { + return CommonResponse.success(officeService.getDispatchDetail(dispatchId)); + } + + /** + *
+     *     사무실 - 배차 상태별 카운트 조회
+     * 
+ * + * @return 상태별 배차 개수 + */ + @Operation(summary = "배차 상태별 카운트 조회", description = "OPEN, ASSIGNED, COMPLETED, CANCELED 상태별 배차 개수를 조회합니다.") + @RequestMapping(path = "/dispatch/summary", method = RequestMethod.GET) + public CommonResponse getDispatchSummary() { + return CommonResponse.success(officeService.getDispatchSummary()); + } + /** *
      *     사무실 - 배차 등록
diff --git a/src/main/java/com/mobility/api/domain/office/dto/response/DispatchSummaryRes.java b/src/main/java/com/mobility/api/domain/office/dto/response/DispatchSummaryRes.java
new file mode 100644
index 0000000..f74c582
--- /dev/null
+++ b/src/main/java/com/mobility/api/domain/office/dto/response/DispatchSummaryRes.java
@@ -0,0 +1,13 @@
+package com.mobility.api.domain.office.dto.response;
+
+import com.mobility.api.domain.dispatch.enums.StatusType;
+
+import java.util.Map;
+
+public record DispatchSummaryRes(
+        Map statusCounts
+) {
+    public static DispatchSummaryRes from(Map counts) {
+        return new DispatchSummaryRes(counts);
+    }
+}
diff --git a/src/main/java/com/mobility/api/domain/office/dto/response/GetDispatchDetailRes.java b/src/main/java/com/mobility/api/domain/office/dto/response/GetDispatchDetailRes.java
new file mode 100644
index 0000000..faa5671
--- /dev/null
+++ b/src/main/java/com/mobility/api/domain/office/dto/response/GetDispatchDetailRes.java
@@ -0,0 +1,141 @@
+package com.mobility.api.domain.office.dto.response;
+
+import com.mobility.api.domain.dispatch.entity.Dispatch;
+import com.mobility.api.domain.dispatch.enums.*;
+import com.mobility.api.domain.transporter.entity.Transporter;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "배차 상세 응답")
+public record GetDispatchDetailRes(
+        @Schema(description = "배차 ID", example = "1")
+        Long id,
+
+        @Schema(description = "배차 번호", example = "2024-0001")
+        String dispatchNumber,
+
+        @Schema(description = "배차 상태", example = "OPEN")
+        StatusType status,
+
+        @Schema(description = "요금 (원)", example = "150000")
+        Integer charge,
+
+        @Schema(description = "출발지 주소", example = "서울특별시 강남구 테헤란로 123")
+        String startLocation,
+
+        @Schema(description = "출발지 위도", example = "37.5065")
+        double startLatitude,
+
+        @Schema(description = "출발지 경도", example = "127.0536")
+        double startLongitude,
+
+        @Schema(description = "도착지 주소", example = "부산광역시 해운대구 우동 456")
+        String destinationLocation,
+
+        @Schema(description = "도착지 위도", example = "35.1595")
+        double destinationLatitude,
+
+        @Schema(description = "도착지 경도", example = "129.1603")
+        double destinationLongitude,
+
+        @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 (미배차 시 null)", example = "1")
+        Long transporterId,
+
+        @Schema(description = "배차된 기사 이름 (미배차 시 null)", example = "홍길동")
+        String transporterName,
+
+        @Schema(description = "배차된 기사 전화번호 (미배차 시 null)", example = "010-1234-5678")
+        String transporterPhone,
+
+        @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 = "배차 할당 일시 (미배차 시 null)", example = "2024-01-15T10:30:00")
+        LocalDateTime assignedAt,
+
+        @Schema(description = "완료 일시 (미완료 시 null)", example = "2024-01-15T12:00:00")
+        LocalDateTime completedAt,
+
+        @Schema(description = "취소 일시 (미취소 시 null)", example = "2024-01-15T11:00:00")
+        LocalDateTime canceledAt,
+
+        @Schema(description = "취소 사유 (최대 200자, 한글/영어 동일)", example = "고객 요청으로 취소", maxLength = 200)
+        String cancelReason
+) {
+    public static GetDispatchDetailRes from(Dispatch dispatch) {
+        Transporter transporter = dispatch.getTransporter();
+
+        return new GetDispatchDetailRes(
+                dispatch.getId(),
+                dispatch.getDispatchNumber(),
+                dispatch.getStatus(),
+                dispatch.getCharge(),
+                dispatch.getStartLocation(),
+                dispatch.getStartLatitude(),
+                dispatch.getStartLongitude(),
+                dispatch.getDestinationLocation(),
+                dispatch.getDestinationLatitude(),
+                dispatch.getDestinationLongitude(),
+                maskPhoneNumber(dispatch.getClientPhoneNumber()),
+                dispatch.getMemo(),
+                dispatch.getCall(),
+                dispatch.getService(),
+                dispatch.getPaymentType(),
+                dispatch.getTollType(),
+                transporter != null ? transporter.getId() : null,
+                transporter != null ? transporter.getName() : null,
+                transporter != null ? transporter.getPhone() : null,
+                dispatch.getOfficeId(),
+                dispatch.getCreatedAt(),
+                dispatch.getUpdatedAt(),
+                dispatch.getAssignedAt(),
+                dispatch.getCompletedAt(),
+                dispatch.getCanceledAt(),
+                dispatch.getCancelReason()
+        );
+    }
+
+    private static String maskPhoneNumber(String phoneNumber) {
+        if (phoneNumber == null || phoneNumber.length() < 8) {
+            return phoneNumber;
+        }
+        // 010-1234-5678 -> 010-****-5678
+        // 01012345678 -> 010****5678
+        if (phoneNumber.contains("-")) {
+            String[] parts = phoneNumber.split("-");
+            if (parts.length == 3) {
+                return parts[0] + "-****-" + parts[2];
+            }
+        }
+        // 하이픈 없는 경우 (01012345678)
+        if (phoneNumber.length() == 11) {
+            return phoneNumber.substring(0, 3) + "****" + phoneNumber.substring(7);
+        }
+        return phoneNumber;
+    }
+}
diff --git a/src/main/java/com/mobility/api/domain/office/service/OfficeService.java b/src/main/java/com/mobility/api/domain/office/service/OfficeService.java
index 911258e..858de71 100644
--- a/src/main/java/com/mobility/api/domain/office/service/OfficeService.java
+++ b/src/main/java/com/mobility/api/domain/office/service/OfficeService.java
@@ -7,7 +7,9 @@
 import com.mobility.api.domain.office.dto.request.CreateDispatchReq;
 import com.mobility.api.domain.office.dto.request.DispatchSearchDto;
 import com.mobility.api.domain.office.dto.request.UpdateDispatchReq;
+import com.mobility.api.domain.office.dto.response.DispatchSummaryRes;
 import com.mobility.api.domain.office.dto.response.GetAllDispatchRes;
+import com.mobility.api.domain.office.dto.response.GetDispatchDetailRes;
 import com.mobility.api.domain.office.entity.Manager;
 import com.mobility.api.domain.office.entity.Office;
 import com.mobility.api.domain.transporter.dto.request.TransporterCreateReq;
@@ -27,8 +29,7 @@
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import java.util.ArrayList;
-import java.util.List;
+import java.util.*;
 
 @Service
 @RequiredArgsConstructor
@@ -75,15 +76,34 @@ public Page findAllDispatch(DispatchSearchDto searchDto, Page
 
     @Transactional
     public void saveDispatch(CreateDispatchReq createDispatchReq) {
-        // 1. 배차 저장
-        Dispatch savedDispatch = dispatchRepository.save(createDispatchReq.toEntity());
+        // 1. 주변 1km 내 자동배차 ON 기사 존재 여부 확인
+        boolean hasEligibleDrivers = transporterRepository.existsEligibleDriversWithinRadius(
+                createDispatchReq.startLatitude(),
+                createDispatchReq.startLongitude()
+        );
 
-        log.info("[Office] 배차 등록 완료 - dispatchId: {}", savedDispatch.getId());
+        // 2. 배차 엔티티 생성
+        Dispatch dispatch = createDispatchReq.toEntity();
 
-        // 2. 자동 배차 알림 시작 (비동기)
-        autoDispatchService.startSequentialNotification(savedDispatch.getId());
+        // 3. 적격 기사 유무에 따라 상태 결정
+        if (hasEligibleDrivers) {
+            // 주변에 자동배차 ON 기사가 있음 → HOLD 상태로 시작
+            dispatch.setStatus(StatusType.HOLD);
+            Dispatch savedDispatch = dispatchRepository.save(dispatch);
 
-        log.info("[Office] 자동 배차 알림 트리거 완료 - dispatchId: {}", savedDispatch.getId());
+            log.info("[Office] 배차 등록 완료 (HOLD) - dispatchId: {}, 주변 적격 기사 있음", savedDispatch.getId());
+
+            // 자동 배차 알림 시작 (비동기)
+            autoDispatchService.startSequentialNotification(savedDispatch.getId());
+
+            log.info("[Office] 자동 배차 알림 트리거 완료 - dispatchId: {}", savedDispatch.getId());
+        } else {
+            // 주변에 자동배차 ON 기사가 없음 → 바로 OPEN 상태
+            dispatch.setStatus(StatusType.OPEN);
+            Dispatch savedDispatch = dispatchRepository.save(dispatch);
+
+            log.info("[Office] 배차 등록 완료 (OPEN) - dispatchId: {}, 주변 적격 기사 없음", savedDispatch.getId());
+        }
     }
 
     @Transactional
@@ -120,10 +140,39 @@ public void cancelDispatch(Long dispatchId) { // <- 메서드 이름도 delete -
 
         // TODO: dispatch.cancel() 같은 엔티티 메서드로 캡슐화
         dispatch.setStatus(StatusType.CANCELED);
+        dispatch.setCanceledAt(java.time.LocalDateTime.now());
 
         // @Transactional이 변경 감지(Dirty Checking)로 UPDATE
     }
 
+    @Transactional(readOnly = true)
+    public GetDispatchDetailRes getDispatchDetail(Long dispatchId) {
+        Dispatch dispatch = dispatchRepository.findById(dispatchId)
+                .orElseThrow(() -> new BusinessException(ApiResponseCode.DISPATCH_NOT_FOUND));
+
+        return GetDispatchDetailRes.from(dispatch);
+    }
+
+    @Transactional(readOnly = true)
+    public DispatchSummaryRes getDispatchSummary() {
+        List results = dispatchRepository.countByStatus();
+
+        // 모든 상태를 0으로 초기화
+        Map statusCounts = new EnumMap<>(StatusType.class);
+        for (StatusType status : StatusType.values()) {
+            statusCounts.put(status, 0L);
+        }
+
+        // 조회 결과로 업데이트
+        for (Object[] row : results) {
+            StatusType status = (StatusType) row[0];
+            Long count = (Long) row[1];
+            statusCounts.put(status, count);
+        }
+
+        return DispatchSummaryRes.from(statusCounts);
+    }
+
     /**
      * 기사 등록 (사장님이 호출)
      * @param req 기사 정보
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 b95deda..8746a22 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
@@ -1,10 +1,10 @@
 package com.mobility.api.domain.transporter.entity;
 
 import com.mobility.api.domain.office.entity.Office;
+import com.mobility.api.global.entity.BaseEntity;
 import jakarta.persistence.*;
 import lombok.*;
-import org.hibernate.annotations.CreationTimestamp;
-import org.hibernate.annotations.UpdateTimestamp;
+import lombok.experimental.SuperBuilder;
 import org.locationtech.jts.geom.Point;
 
 import java.time.LocalDateTime;
@@ -13,9 +13,9 @@
 @Getter
 @NoArgsConstructor(access = AccessLevel.PROTECTED)
 @AllArgsConstructor
-@Builder
+@SuperBuilder
 @Table(name = "transporters")
-public class Transporter {
+public class Transporter extends BaseEntity {
 
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -39,13 +39,4 @@ public class Transporter {
     @ManyToOne(fetch = FetchType.LAZY)
     @JoinColumn(name = "office_id")    // DB 컬럼명: office_id
     private Office office;
-
-    @CreationTimestamp
-    @Column(name = "created_at", updatable = false)
-    private LocalDateTime createdAt;
-
-    @UpdateTimestamp
-    @Column(name = "update_at")
-    private LocalDateTime updatedAt;
-
 }
diff --git a/src/main/java/com/mobility/api/domain/transporter/repository/TransporterRepository.java b/src/main/java/com/mobility/api/domain/transporter/repository/TransporterRepository.java
index 0f9cc42..85fe4fe 100644
--- a/src/main/java/com/mobility/api/domain/transporter/repository/TransporterRepository.java
+++ b/src/main/java/com/mobility/api/domain/transporter/repository/TransporterRepository.java
@@ -59,6 +59,23 @@ List findEligibleDriversForAutoDispatch(
             @Param("lon") double lon
     );
 
+    /**
+     * 1km 반경 내 자동배차 ON 기사 존재 여부 확인
+     */
+    @Query(value = """
+        SELECT COUNT(*) > 0
+        FROM transporters t
+        WHERE t.current_location IS NOT NULL
+          AND t.is_auto_dispatch = true
+          AND ST_DWithin(t.current_location::geography,
+                         ST_SetSRID(ST_MakePoint(:lon, :lat), 4326)::geography,
+                         1000)
+        """, nativeQuery = true)
+    boolean existsEligibleDriversWithinRadius(
+            @Param("lat") double lat,
+            @Param("lon") double lon
+    );
+
     // 전화번호 중복 가입 체크용
     boolean existsByPhone(String phoneNumber);
 
diff --git a/src/main/java/com/mobility/api/global/entity/BaseEntity.java b/src/main/java/com/mobility/api/global/entity/BaseEntity.java
new file mode 100644
index 0000000..7c481c9
--- /dev/null
+++ b/src/main/java/com/mobility/api/global/entity/BaseEntity.java
@@ -0,0 +1,29 @@
+package com.mobility.api.global.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.EntityListeners;
+import jakarta.persistence.MappedSuperclass;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+import org.hibernate.annotations.CreationTimestamp;
+import org.hibernate.annotations.UpdateTimestamp;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import java.time.LocalDateTime;
+
+@Getter
+@MappedSuperclass
+@EntityListeners(AuditingEntityListener.class)
+@SuperBuilder
+@NoArgsConstructor
+public abstract class BaseEntity {
+
+    @CreationTimestamp
+    @Column(name = "created_at", updatable = false)
+    private LocalDateTime createdAt;
+
+    @UpdateTimestamp
+    @Column(name = "updated_at")
+    private LocalDateTime updatedAt;
+}
\ No newline at end of file
diff --git a/src/test/java/com/mobility/api/domain/dispatch/controller/DispatchV1ControllerTest.java b/src/test/java/com/mobility/api/domain/dispatch/controller/DispatchV1ControllerTest.java
index c8d9e73..adfa768 100644
--- a/src/test/java/com/mobility/api/domain/dispatch/controller/DispatchV1ControllerTest.java
+++ b/src/test/java/com/mobility/api/domain/dispatch/controller/DispatchV1ControllerTest.java
@@ -5,15 +5,19 @@
 import com.mobility.api.domain.dispatch.entity.Dispatch;
 import com.mobility.api.domain.dispatch.repository.DispatchRepository;
 import com.mobility.api.domain.dispatch.service.DispatcherService;
+import com.mobility.api.domain.office.repository.ManagerRepository;
 import com.mobility.api.domain.transporter.dto.response.TransporterMatchResponse;
 import com.mobility.api.domain.transporter.repository.TransporterRepository;
 import com.mobility.api.domain.transporter.service.TransporterService;
+import com.mobility.api.global.jwt.JwtProvider;
+import com.mobility.api.global.security.CustomUserDetailsService;
 import com.mobility.api.global.exception.GlobalException;
 import com.mobility.api.global.response.ResultCode;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext;
 import org.springframework.security.test.context.support.WithMockUser;
 import org.springframework.test.context.bean.override.mockito.MockitoBean;
 import org.springframework.http.MediaType;
@@ -48,6 +52,18 @@ class DispatchV1ControllerTest {
     @MockitoBean
     private DispatcherService dispatcherService;
 
+    @MockitoBean
+    private ManagerRepository managerRepository;
+
+    @MockitoBean
+    private JwtProvider jwtProvider;
+
+    @MockitoBean
+    private CustomUserDetailsService customUserDetailsService;
+
+    @MockitoBean
+    private JpaMetamodelMappingContext jpaMetamodelMappingContext;
+
     @Test
     @DisplayName("[API] 배차 주변 기사 조회 - 성공 (200 OK)")
     void getNearbyDrivers_ApiSuccess() throws Exception {