From 2b6ee144530ff23515d36d6dd5b7ea50bf97819d Mon Sep 17 00:00:00 2001 From: SOWON LEE <66356241+Leesowon@users.noreply.github.com> Date: Fri, 9 Jan 2026 15:54:42 +0900 Subject: [PATCH 1/9] =?UTF-8?q?[#51]=20feat:=20BaseEntity=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mobility/api/MobilityApiApplication.java | 2 ++ .../api/global/entity/BaseEntity.java | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 src/main/java/com/mobility/api/global/entity/BaseEntity.java 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/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 From 9628e50b07e91aeea854cfc80c6382d7382433a7 Mon Sep 17 00:00:00 2001 From: SOWON LEE <66356241+Leesowon@users.noreply.github.com> Date: Fri, 9 Jan 2026 15:55:05 +0900 Subject: [PATCH 2/9] =?UTF-8?q?[#51]=20feat:=20Dispatch=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=EC=97=90=20=EC=83=81=ED=83=9C=20=EC=B6=94?= =?UTF-8?q?=EC=A0=81=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/domain/dispatch/entity/Dispatch.java | 16 +++++++++++++--- .../api/domain/office/service/OfficeService.java | 10 ++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) 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..b6b33fa 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,9 +17,9 @@ @Setter @NoArgsConstructor @AllArgsConstructor -@Builder +@SuperBuilder @Slf4j -public class Dispatch { +public class Dispatch extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -67,7 +69,13 @@ 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; // 취소 시간 // 기사 배차 시 public void assignDispatch(Transporter transporter) { @@ -79,6 +87,7 @@ public void assignDispatch(Transporter transporter) { this.transporter = transporter; this.status = StatusType.ASSIGNED; + this.assignedAt = LocalDateTime.now(); } public void cancelDispatch(Transporter transporter) { @@ -100,6 +109,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/office/service/OfficeService.java b/src/main/java/com/mobility/api/domain/office/service/OfficeService.java index 0d85063..7ccd296 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 @@ -8,6 +8,7 @@ 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.GetAllDispatchRes; +import com.mobility.api.domain.office.dto.response.GetDispatchDetailRes; import com.mobility.api.global.enums.ApiResponseCode; import com.mobility.api.global.exception.BusinessException; import com.mobility.api.global.exception.GlobalException; @@ -113,8 +114,17 @@ 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); + } + } From ed471e8780ed1cbe08ef435b7de23627d319bc40 Mon Sep 17 00:00:00 2001 From: SOWON LEE <66356241+Leesowon@users.noreply.github.com> Date: Fri, 9 Jan 2026 15:55:30 +0900 Subject: [PATCH 3/9] =?UTF-8?q?[#51]=20refactor:=20Transporter=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20BaseEntity=20=EC=83=81=EC=86=8D=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/transporter/entity/Transporter.java | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) 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 626f5c5..041fd28 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,20 +1,18 @@ package com.mobility.api.domain.transporter.entity; +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; - @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor -@Builder +@SuperBuilder @Table(name = "transporters") -public class Transporter { +public class Transporter extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -35,12 +33,4 @@ public class Transporter { @Column(name = "is_auto_dispatch") private boolean isAutoDispatch; - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; - - @UpdateTimestamp - @Column(name = "update_at") - private LocalDateTime updatedAt; - } From 68e9da81cc3e8a7f77fe872d2f69b51543b77766 Mon Sep 17 00:00:00 2001 From: SOWON LEE <66356241+Leesowon@users.noreply.github.com> Date: Fri, 9 Jan 2026 15:55:49 +0900 Subject: [PATCH 4/9] =?UTF-8?q?[#51]=20feat:=20=EC=82=AC=EB=AC=B4=EC=8B=A4?= =?UTF-8?q?=20=EB=B0=B0=EC=B0=A8=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../office/controller/OfficeV1Controller.java | 17 ++++ .../dto/response/GetDispatchDetailRes.java | 88 +++++++++++++++++++ .../controller/DispatchV1ControllerTest.java | 8 ++ 3 files changed, 113 insertions(+) create mode 100644 src/main/java/com/mobility/api/domain/office/dto/response/GetDispatchDetailRes.java 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 c28aa17..77a825b 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 @@ -5,6 +5,7 @@ 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.GetAllDispatchRes; +import com.mobility.api.domain.office.dto.response.GetDispatchDetailRes; import com.mobility.api.domain.office.service.OfficeService; import com.mobility.api.global.annotation.SwaggerPageable; import com.mobility.api.global.response.CommonResponse; @@ -44,6 +45,22 @@ 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)); + } + /** *
      *     사무실 - 배차 등록
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..867e126
--- /dev/null
+++ b/src/main/java/com/mobility/api/domain/office/dto/response/GetDispatchDetailRes.java
@@ -0,0 +1,88 @@
+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 java.time.LocalDateTime;
+
+public record GetDispatchDetailRes(
+        Long id,
+        String dispatchNumber,
+        StatusType status,
+        Integer charge,
+        String startLocation,
+        double startLatitude,
+        double startLongitude,
+        String destinationLocation,
+        double destinationLatitude,
+        double destinationLongitude,
+        String clientPhoneNumber,
+        String memo,
+        CallType call,
+        ServiceType service,
+        PaymentType paymentMethod,
+        TollType tollType,
+        Long transporterId,
+        String transporterName,
+        String transporterPhone,
+        Long officeId,
+        LocalDateTime createdAt,
+        LocalDateTime updatedAt,
+        LocalDateTime assignedAt,
+        LocalDateTime completedAt,
+        LocalDateTime canceledAt,
+        String cancelReason
+) {
+    public static GetDispatchDetailRes from(Dispatch dispatch) {
+        Transporter transporter = dispatch.getTransporter();
+
+        return new GetDispatchDetailRes(
+                dispatch.getId(),
+                null, // dispatchNumber - 엔티티에 없음
+                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(),
+                null // cancelReason - 엔티티에 없음
+        );
+    }
+
+    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/test/java/com/mobility/api/domain/dispatch/controller/DispatchV1ControllerTest.java b/src/test/java/com/mobility/api/domain/dispatch/controller/DispatchV1ControllerTest.java
index c8d9e73..566770b 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,9 +5,11 @@
 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.exception.GlobalException;
 import com.mobility.api.global.response.ResultCode;
 import org.junit.jupiter.api.DisplayName;
@@ -48,6 +50,12 @@ class DispatchV1ControllerTest {
     @MockitoBean
     private DispatcherService dispatcherService;
 
+    @MockitoBean
+    private ManagerRepository managerRepository;
+
+    @MockitoBean
+    private JwtProvider jwtProvider;
+
     @Test
     @DisplayName("[API] 배차 주변 기사 조회 - 성공 (200 OK)")
     void getNearbyDrivers_ApiSuccess() throws Exception {

From ae0883cab824064ffa6efcf48a3ecf14d6b4ba64 Mon Sep 17 00:00:00 2001
From: SOWON LEE <66356241+Leesowon@users.noreply.github.com>
Date: Fri, 9 Jan 2026 16:25:53 +0900
Subject: [PATCH 5/9] =?UTF-8?q?[#51]=20feat:=20=EB=B0=B0=EC=B0=A8=20?=
 =?UTF-8?q?=EC=83=81=ED=83=9C=EB=B3=84=20=EC=B9=B4=EC=9A=B4=ED=8A=B8=20api?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../repository/DispatchRepository.java        | 14 +++++++++++
 .../office/controller/OfficeV1Controller.java | 14 +++++++++++
 .../dto/response/DispatchSummaryRes.java      | 13 ++++++++++
 .../domain/office/service/OfficeService.java  | 24 +++++++++++++++++--
 4 files changed, 63 insertions(+), 2 deletions(-)
 create mode 100644 src/main/java/com/mobility/api/domain/office/dto/response/DispatchSummaryRes.java

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/office/controller/OfficeV1Controller.java b/src/main/java/com/mobility/api/domain/office/controller/OfficeV1Controller.java
index 77a825b..55d99a9 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,6 +4,7 @@
 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;
@@ -61,6 +62,19 @@ public CommonResponse getDispatchDetail(
         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/service/OfficeService.java b/src/main/java/com/mobility/api/domain/office/service/OfficeService.java
index 7ccd296..dad6520 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,6 +7,7 @@
 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.global.enums.ApiResponseCode;
@@ -22,8 +23,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
@@ -127,4 +127,24 @@ public GetDispatchDetailRes getDispatchDetail(Long dispatchId) {
         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);
+    }
+
 }

From 49d0c58820bd001a107b6bd52520d36c13e28402 Mon Sep 17 00:00:00 2001
From: SOWON LEE <66356241+Leesowon@users.noreply.github.com>
Date: Fri, 9 Jan 2026 16:58:18 +0900
Subject: [PATCH 6/9] =?UTF-8?q?[#51]=20feat:=20HOLD=20=EC=83=81=ED=83=9C?=
 =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=9E=90=EB=8F=99=EB=B0=B0?=
 =?UTF-8?q?=EC=B0=A8=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../api/domain/dispatch/entity/Dispatch.java  |  4 +--
 .../api/domain/dispatch/enums/StatusType.java |  7 ++--
 .../dispatch/service/AutoDispatchService.java | 27 +++++++++++----
 .../domain/office/service/OfficeService.java  | 33 +++++++++++++++----
 .../repository/TransporterRepository.java     | 17 ++++++++++
 5 files changed, 71 insertions(+), 17 deletions(-)

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 b6b33fa..3f653d6 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
@@ -80,8 +80,8 @@ public class Dispatch extends BaseEntity {
     // 기사 배차 시
     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);
         }
 
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/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/service/OfficeService.java b/src/main/java/com/mobility/api/domain/office/service/OfficeService.java
index dad6520..f88a94b 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
@@ -4,6 +4,7 @@
 import com.mobility.api.domain.dispatch.enums.StatusType;
 import com.mobility.api.domain.dispatch.repository.DispatchRepository;
 import com.mobility.api.domain.dispatch.service.AutoDispatchService;
+import com.mobility.api.domain.transporter.repository.TransporterRepository;
 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;
@@ -31,6 +32,7 @@
 public class OfficeService {
 
     private final DispatchRepository dispatchRepository;
+    private final TransporterRepository transporterRepository;
     private final AutoDispatchService autoDispatchService;
 
     public Page findAllDispatch(DispatchSearchDto searchDto, Pageable pageable) {
@@ -69,15 +71,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
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 a55ddce..864af4f 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
@@ -58,6 +58,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);
 

From 5542826bb999a3189d3411213b08469251f35075 Mon Sep 17 00:00:00 2001
From: SOWON LEE <66356241+Leesowon@users.noreply.github.com>
Date: Tue, 13 Jan 2026 23:45:45 +0900
Subject: [PATCH 7/9] =?UTF-8?q?[#51]=20feat:=20=EC=82=AC=EB=AC=B4=EC=8B=A4?=
 =?UTF-8?q?-=EB=B0=B0=EC=B0=A8=20=EB=8B=A8=EC=9D=BC=20=EC=A1=B0=ED=9A=8C?=
 =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=EA=B0=92=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../api/domain/dispatch/entity/Dispatch.java  |  5 ++
 .../dto/response/GetDispatchDetailRes.java    | 57 ++++++++++++++++++-
 2 files changed, 60 insertions(+), 2 deletions(-)

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 3f653d6..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
@@ -25,6 +25,8 @@ public class Dispatch extends BaseEntity {
     @GeneratedValue(strategy = GenerationType.IDENTITY)
     private Long id;
 
+    private String dispatchNumber; // 배차 번호 (예: 2024-0001)
+
     private String startLocation; // 출발지
 
     private double startLatitude; // 출발지 위도
@@ -77,6 +79,9 @@ public class Dispatch extends BaseEntity {
 
     private LocalDateTime canceledAt; // 취소 시간
 
+    @Column(length = 500)
+    private String cancelReason; // 취소 사유 (최대 200자)
+
     // 기사 배차 시
     public void assignDispatch(Transporter transporter) {
 
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
index 867e126..faa5671 100644
--- 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
@@ -3,35 +3,88 @@
 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) {
@@ -39,7 +92,7 @@ public static GetDispatchDetailRes from(Dispatch dispatch) {
 
         return new GetDispatchDetailRes(
                 dispatch.getId(),
-                null, // dispatchNumber - 엔티티에 없음
+                dispatch.getDispatchNumber(),
                 dispatch.getStatus(),
                 dispatch.getCharge(),
                 dispatch.getStartLocation(),
@@ -63,7 +116,7 @@ public static GetDispatchDetailRes from(Dispatch dispatch) {
                 dispatch.getAssignedAt(),
                 dispatch.getCompletedAt(),
                 dispatch.getCanceledAt(),
-                null // cancelReason - 엔티티에 없음
+                dispatch.getCancelReason()
         );
     }
 

From 8aadf6b04ba31a3d50667fdd6f73b349e7cb30c7 Mon Sep 17 00:00:00 2001
From: SOWON LEE <66356241+Leesowon@users.noreply.github.com>
Date: Wed, 14 Jan 2026 00:01:03 +0900
Subject: [PATCH 8/9] =?UTF-8?q?[#51]=20fix:=20OfficeService=20import=20?=
 =?UTF-8?q?=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/mobility/api/domain/office/service/OfficeService.java   | 2 ++
 1 file changed, 2 insertions(+)

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 b6644cf..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;

From 4282ee9eff8b453874359031fce18675e1075f45 Mon Sep 17 00:00:00 2001
From: SOWON LEE <66356241+Leesowon@users.noreply.github.com>
Date: Wed, 14 Jan 2026 00:09:44 +0900
Subject: [PATCH 9/9] =?UTF-8?q?[#51]=20fix:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?=
 =?UTF-8?q?=EC=97=90=20=EB=88=84=EB=9D=BD=EB=90=9C=20Mock=20Bean=20?=
 =?UTF-8?q?=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../dispatch/controller/DispatchV1ControllerTest.java     | 8 ++++++++
 1 file changed, 8 insertions(+)

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 566770b..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
@@ -10,12 +10,14 @@
 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;
@@ -56,6 +58,12 @@ class DispatchV1ControllerTest {
     @MockitoBean
     private JwtProvider jwtProvider;
 
+    @MockitoBean
+    private CustomUserDetailsService customUserDetailsService;
+
+    @MockitoBean
+    private JpaMetamodelMappingContext jpaMetamodelMappingContext;
+
     @Test
     @DisplayName("[API] 배차 주변 기사 조회 - 성공 (200 OK)")
     void getNearbyDrivers_ApiSuccess() throws Exception {