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
Expand Up @@ -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;

Expand Down Expand Up @@ -64,4 +66,12 @@ List<DispatchDistanceProjection> findDispatchesByDistance(
*/
@Query("SELECT d.status, COUNT(d) FROM Dispatch d WHERE d.officeId = :officeId GROUP BY d.status")
List<Object[]> 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<Dispatch> findAllWithTransporter(Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -82,6 +83,67 @@ public CommonResponse<DispatchSummaryRes> getDispatchSummary() {
return CommonResponse.success(officeService.getDispatchSummary());
}

/**
* <pre>
* 사무실 - 대시보드 실시간 피드 조회
* </pre>
*
* @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<List<DispatchFeedRes>> getDispatchFeed(
@Parameter(
description = "조회할 피드 개수 (기본값: 20, 최대 권장: 100)",
example = "20",
required = false
)
@RequestParam(required = false, defaultValue = "20") Integer limit
) {
return CommonResponse.success(officeService.getDispatchFeed(limit));
}

/**
* <pre>
* 사무실 - 배차 등록
Expand Down Expand Up @@ -206,10 +268,7 @@ public CommonResponse<Integer> changeTransporterStatus(
user.getManager()
);


return CommonResponse.success(0);
}



}
Original file line number Diff line number Diff line change
@@ -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
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -254,4 +255,62 @@ public void changeTransporterStatus(Long transporterId, TransporterStatus status

}

/**
* 대시보드 실시간 피드 조회
* @param limit 조회 개수 (기본: 20)
* @return 최근 배차 이벤트 피드 목록
*/
@Transactional(readOnly = true)
public List<DispatchFeedRes> getDispatchFeed(Integer limit) {
// 최근 배차 조회 (Transporter와 Fetch Join으로 N+1 문제 해결, createdAt 기준 내림차순)
List<Dispatch> 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<Dispatch> 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();
}

}