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 ce28781..91d4f80 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 @@ -4,6 +4,8 @@ import com.mobility.api.domain.dispatch.entity.Dispatch; import jakarta.persistence.LockModeType; import jakarta.persistence.QueryHint; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.*; import org.springframework.data.repository.query.Param; @@ -64,4 +66,12 @@ List findDispatchesByDistance( */ @Query("SELECT d.status, COUNT(d) FROM Dispatch d WHERE d.officeId = :officeId GROUP BY d.status") List countByStatusAndOfficeId(@Param("officeId") Long officeId); + + /** + * 배차 목록 조회 (Transporter와 Fetch Join으로 N+1 문제 해결) + * @param pageable 페이징 및 정렬 정보 + * @return 배차 목록 (Transporter 포함) + */ + @Query("SELECT d FROM Dispatch d LEFT JOIN FETCH d.transporter") + Page findAllWithTransporter(Pageable pageable); } 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 e163bb9..7097767 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.DispatchFeedRes; 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; @@ -82,6 +83,67 @@ public CommonResponse getDispatchSummary() { return CommonResponse.success(officeService.getDispatchSummary()); } + /** + *
+     *     사무실 - 대시보드 실시간 피드 조회
+     * 
+ * + * @param limit 조회 개수 (기본: 20) + * @return 최근 배차 이벤트 피드 목록 + */ + @Operation( + summary = "대시보드 실시간 피드 조회", + description = """ + 최근 배차 이벤트를 시간순으로 조회합니다. + + **피드 타입:** + - `open`: 배차 등록 + - `assigned`: 배차 할당 (기사 배정) + - `completed`: 운송 완료 + - `canceled`: 배차 취소 + + **특징:** + - HOLD 상태(자동배차 진행 중)는 제외됩니다. + - 최신순 정렬 (createdAt DESC) + - Transporter 정보 포함 (N+1 최적화 적용) + + **응답 예시:** + ```json + { + "code": "SUCCESS", + "data": [ + { + "id": "feed-01", + "type": "assigned", + "dispatchId": 123, + "dispatchNumber": "2024-0001", + "transporterName": "김철수", + "message": "김철수 기사가 콜 #2024-0001을 배차 받았습니다", + "timestamp": "2024-01-15T10:32:00" + } + ] + } + ``` + """ + ) + @io.swagger.v3.oas.annotations.responses.ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "조회 성공" + ) + }) + @RequestMapping(path = "/dispatch/feed", method = RequestMethod.GET) + public CommonResponse> getDispatchFeed( + @Parameter( + description = "조회할 피드 개수 (기본값: 20, 최대 권장: 100)", + example = "20", + required = false + ) + @RequestParam(required = false, defaultValue = "20") Integer limit + ) { + return CommonResponse.success(officeService.getDispatchFeed(limit)); + } + /** *
      *     사무실 - 배차 등록
@@ -206,10 +268,7 @@ public CommonResponse changeTransporterStatus(
                 user.getManager()
         );
 
-
         return CommonResponse.success(0);
     }
 
-
-
 }
diff --git a/src/main/java/com/mobility/api/domain/office/dto/response/DispatchFeedRes.java b/src/main/java/com/mobility/api/domain/office/dto/response/DispatchFeedRes.java
new file mode 100644
index 0000000..c2e83ac
--- /dev/null
+++ b/src/main/java/com/mobility/api/domain/office/dto/response/DispatchFeedRes.java
@@ -0,0 +1,79 @@
+package com.mobility.api.domain.office.dto.response;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Builder;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "대시보드 배차 피드 응답 DTO - 최근 배차 이벤트 정보")
+@Builder
+public record DispatchFeedRes(
+        @Schema(
+                description = "피드 고유 ID (연번 형식: feed-01, feed-02, ...)",
+                example = "feed-01",
+                requiredMode = Schema.RequiredMode.REQUIRED
+        )
+        String id,
+
+        @Schema(
+                description = """
+                        피드 타입
+                        - open: 배차 등록
+                        - assigned: 배차 할당 (기사 배정)
+                        - completed: 운송 완료
+                        - canceled: 배차 취소
+                        """,
+                example = "assigned",
+                allowableValues = {"open", "assigned", "completed", "canceled"},
+                requiredMode = Schema.RequiredMode.REQUIRED
+        )
+        String type,
+
+        @Schema(
+                description = "배차 ID (Dispatch 테이블의 PK)",
+                example = "123",
+                requiredMode = Schema.RequiredMode.REQUIRED
+        )
+        Long dispatchId,
+
+        @Schema(
+                description = "배차 번호 (예: 2024-0001) - null일 수 있음",
+                example = "2024-0001",
+                nullable = true
+        )
+        String dispatchNumber,
+
+        @Schema(
+                description = "기사 이름 - assigned, completed 타입일 때만 값이 있고, open, canceled 타입일 때는 null",
+                example = "김철수",
+                nullable = true
+        )
+        String transporterName,
+
+        @Schema(
+                description = """
+                        자동 생성된 피드 메시지
+                        - open: "배차 #{번호}가 등록되었습니다"
+                        - assigned: "{기사명} 기사가 콜 #{번호}을 배차 받았습니다"
+                        - completed: "{기사명} 기사가 콜 #{번호}을 완료했습니다"
+                        - canceled: "배차 #{번호}이 취소되었습니다"
+                        """,
+                example = "김철수 기사가 콜 #2024-0001을 배차 받았습니다",
+                requiredMode = Schema.RequiredMode.REQUIRED
+        )
+        String message,
+
+        @Schema(
+                description = """
+                        이벤트 발생 시간 (타입별 타임스탬프)
+                        - open: createdAt (배차 생성 시간)
+                        - assigned: assignedAt (배차 할당 시간)
+                        - completed: completedAt (운송 완료 시간)
+                        - canceled: canceledAt (취소 시간)
+                        """,
+                example = "2024-01-15T10:32:00",
+                requiredMode = Schema.RequiredMode.REQUIRED
+        )
+        LocalDateTime timestamp
+) {
+}
\ No newline at end of file
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 3c0722e..d832b64 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.DispatchFeedRes;
 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;
@@ -254,4 +255,62 @@ public void changeTransporterStatus(Long transporterId, TransporterStatus status
 
     }
 
+    /**
+     * 대시보드 실시간 피드 조회
+     * @param limit 조회 개수 (기본: 20)
+     * @return 최근 배차 이벤트 피드 목록
+     */
+    @Transactional(readOnly = true)
+    public List getDispatchFeed(Integer limit) {
+        // 최근 배차 조회 (Transporter와 Fetch Join으로 N+1 문제 해결, createdAt 기준 내림차순)
+        List recentDispatches = dispatchRepository.findAllWithTransporter(
+                org.springframework.data.domain.PageRequest.of(0, limit,
+                        org.springframework.data.domain.Sort.by(org.springframework.data.domain.Sort.Direction.DESC, "createdAt"))
+        ).getContent();
+
+        // HOLD 상태 제외 후 리스트로 변환
+        List filteredDispatches = recentDispatches.stream()
+                .filter(dispatch -> dispatch.getStatus() != StatusType.HOLD)
+                .toList();
+
+        // 각 배차를 피드 DTO로 변환 (연번 부여)
+        return java.util.stream.IntStream.range(0, filteredDispatches.size())
+                .mapToObj(index -> {
+                    Dispatch dispatch = filteredDispatches.get(index);
+                    String feedId = String.format("feed-%02d", index + 1); // feed-01, feed-02, ...
+                    String type = dispatch.getStatus().name().toLowerCase();
+                    String transporterName = (dispatch.getTransporter() != null) ? dispatch.getTransporter().getName() : null;
+                    String dispatchNumberDisplay = (dispatch.getDispatchNumber() != null) ? "#" + dispatch.getDispatchNumber() : "#" + dispatch.getId();
+
+                    // 상태별 타임스탬프 선택
+                    java.time.LocalDateTime timestamp = switch (dispatch.getStatus()) {
+                        case OPEN -> dispatch.getCreatedAt();
+                        case ASSIGNED -> dispatch.getAssignedAt();
+                        case COMPLETED -> dispatch.getCompletedAt();
+                        case CANCELED -> dispatch.getCanceledAt();
+                        default -> dispatch.getCreatedAt();
+                    };
+
+                    // 상태별 메시지 생성
+                    String message = switch (dispatch.getStatus()) {
+                        case OPEN -> "배차 " + dispatchNumberDisplay + "가 등록되었습니다";
+                        case ASSIGNED -> transporterName + " 기사가 콜 " + dispatchNumberDisplay + "을 배차 받았습니다";
+                        case COMPLETED -> transporterName + " 기사가 콜 " + dispatchNumberDisplay + "을 완료했습니다";
+                        case CANCELED -> "배차 " + dispatchNumberDisplay + "이 취소되었습니다";
+                        default -> "배차 " + dispatchNumberDisplay;
+                    };
+
+                    return DispatchFeedRes.builder()
+                            .id(feedId)
+                            .type(type)
+                            .dispatchId(dispatch.getId())
+                            .dispatchNumber(dispatch.getDispatchNumber())
+                            .transporterName(transporterName)
+                            .message(message)
+                            .timestamp(timestamp)
+                            .build();
+                })
+                .toList();
+    }
+
 }