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
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ jobs:
- name: Gradle 권한 부여
run: chmod +x ./gradlew

- name: Gradle 빌드 실행
run: ./gradlew clean build --stacktrace
- name: 컴파일 체크
run: ./gradlew clean compileJava compileTestJava --stacktrace

- name: 단위 테스트 실행 (통합 테스트 제외)
run: ./gradlew test --tests "*ServiceTest" --tests "*ControllerTest" --stacktrace

- name: Gradle 캐시 설정
uses: actions/cache@v4
Expand Down
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'

// TestContainers for PostgreSQL + PostGIS
testImplementation 'org.testcontainers:testcontainers:1.19.3'
testImplementation 'org.testcontainers:postgresql:1.19.3'
testImplementation 'org.testcontainers:junit-jupiter:1.19.3'

// 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,120 @@
package com.mobility.api.domain.dispatch.dto.response;

import com.mobility.api.domain.dispatch.entity.Dispatch;
import com.mobility.api.domain.dispatch.enums.PaymentType;
import com.mobility.api.domain.dispatch.enums.ServiceType;
import com.mobility.api.domain.dispatch.enums.TollType;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Builder
@Schema(description = "완료된 배차 상세 정보 응답")
public record CompletedDispatchDetailRes(
@Schema(description = "배차 ID", example = "1")
Long id,

@Schema(description = "사무실 이름", example = "태) (주)대리GO")
String officeName,

@Schema(description = "사무실 전화번호", example = "16887141")
String officeTelNumber,

@Schema(description = "출발지", example = "부안상서면부장1길 23")
String startLocation,

@Schema(description = "목적지", example = "수원평동, 임광모터스")
String destinationLocation,

@Schema(description = "요금", example = "110000")
Integer charge,

@Schema(description = "서비스 타입 (DELIVERY: 탁송, DRIVER: 대리)", example = "DELIVERY")
String serviceType,

@Schema(description = "배차 번호", example = "2025-0001")
String dispatchNumber,

@Schema(description = "배차 생성 시간", example = "2025-11-22T11:43:00")
LocalDateTime createdAt,

@Schema(description = "배차 할당 시간", example = "2025-11-22T11:48:00")
LocalDateTime assignedAt,

@Schema(description = "배차 완료 시간", example = "2025-11-22T15:19:00")
LocalDateTime completedAt,

@Schema(description = "차량 타입", example = "null", nullable = true)
String carType,

@Schema(description = "차량 번호", example = "null", nullable = true)
String carNumber,

@Schema(description = "태그 목록 (결제 방식, 톨게이트 방식 등)", example = "[\"현금\", \"톨포\"]")
List<String> tags
) {
public static CompletedDispatchDetailRes from(Dispatch dispatch, String officeName, String officeTelNumber) {
return CompletedDispatchDetailRes.builder()
.id(dispatch.getId())
.officeName(officeName)
.officeTelNumber(officeTelNumber)
.startLocation(dispatch.getStartLocation())
.destinationLocation(dispatch.getDestinationLocation())
.charge(dispatch.getCharge())
.serviceType(dispatch.getService() != null ? dispatch.getService().name() : null)
.dispatchNumber(dispatch.getDispatchNumber())
.createdAt(dispatch.getCreatedAt())
.assignedAt(dispatch.getAssignedAt())
.completedAt(dispatch.getCompletedAt())
.carType(null) // TODO: 차량 타입 필드 추가 시 매핑
.carNumber(null) // TODO: 차량 번호 필드 추가 시 매핑
.tags(buildTags(dispatch))
.build();
}

/**
* 배차 정보로부터 태그 목록 생성
* - 결제 방식 (현금, 후불, 완후)
* - 톨게이트 방식 (톨포, 톨별, 하이패스)
*/
private static List<String> buildTags(Dispatch dispatch) {
List<String> tags = new ArrayList<>();

// 결제 방식 태그
if (dispatch.getPaymentType() != null) {
tags.add(getPaymentTypeLabel(dispatch.getPaymentType()));
}

// 톨게이트 방식 태그
if (dispatch.getTollType() != null) {
tags.add(getTollTypeLabel(dispatch.getTollType()));
}

return tags;
}

/**
* PaymentType enum을 한글 라벨로 변환
*/
private static String getPaymentTypeLabel(PaymentType paymentType) {
return switch (paymentType) {
case CASH -> "현금";
case POSTPAID -> "후불";
case COMPLETE_POSTPAID -> "완후";
};
}

/**
* TollType enum을 한글 라벨로 변환
*/
private static String getTollTypeLabel(TollType tollType) {
return switch (tollType) {
case TOLLGATE_INCLUDED -> "톨포";
case TOLLGATE_SEPARATE -> "톨별";
case HIPASS -> "하이패스";
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.mobility.api.domain.dispatch.dto.response;

import com.mobility.api.domain.dispatch.entity.Dispatch;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

import java.time.LocalDateTime;

@Builder
@Schema(description = "완료된 배차 목록 항목 응답")
public record CompletedDispatchListItemRes(
@Schema(description = "배차 ID", example = "1")
Long id,

@Schema(description = "배차 할당 시간", example = "2025-11-22T14:15:00")
LocalDateTime assignedAt,

@Schema(description = "출발지", example = "부안상서면부장1길 23")
String startLocation,

@Schema(description = "목적지", example = "수원평동, 임광모터스")
String destinationLocation,

@Schema(description = "요금", example = "110000")
Integer charge
) {
public static CompletedDispatchListItemRes from(Dispatch dispatch) {
return CompletedDispatchListItemRes.builder()
.id(dispatch.getId())
.assignedAt(dispatch.getAssignedAt())
.startLocation(dispatch.getStartLocation())
.destinationLocation(dispatch.getDestinationLocation())
.charge(dispatch.getCharge())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ public record CurrentDispatchDetailRes(
@Schema(description = "사무실 ID", example = "1")
Long officeId,

@Schema(description = "사무실 전화번호", example = "02-1234-5678")
String officeTelNumber,

@Schema(description = "생성일시", example = "2024-01-15T10:00:00")
LocalDateTime createdAt,

Expand All @@ -65,9 +68,10 @@ public record CurrentDispatchDetailRes(
/**
* Entity -> DTO 변환 메서드
* @param dispatch 배차 엔티티
* @param officeTelNumber 사무실 전화번호
* @return CurrentDispatchDetailRes
*/
public static CurrentDispatchDetailRes from(Dispatch dispatch) {
public static CurrentDispatchDetailRes from(Dispatch dispatch, String officeTelNumber) {
return CurrentDispatchDetailRes.builder()
.id(dispatch.getId())
.status(dispatch.getStatus())
Expand All @@ -81,6 +85,7 @@ public static CurrentDispatchDetailRes from(Dispatch dispatch) {
.paymentMethod(dispatch.getPaymentType())
.tollType(dispatch.getTollType())
.officeId(dispatch.getOfficeId())
.officeTelNumber(officeTelNumber)
.createdAt(dispatch.getCreatedAt())
.updatedAt(dispatch.getUpdatedAt())
.assignedAt(dispatch.getAssignedAt())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,17 @@ public interface DispatchOfferRepository extends JpaRepository<DispatchOffer, Lo
*/
@Query("SELECT COUNT(o) FROM DispatchOffer o WHERE o.transporter.id = :transporterId AND o.status = 'PENDING'")
int countPendingOffersByTransporter(@Param("transporterId") Long transporterId);

/**
* 기사의 ACCEPTED 상태 제안 조회
* - 한 기사가 하나의 배차만 수락할 수 있도록 검증용
*/
@Query("SELECT o FROM DispatchOffer o WHERE o.transporter.id = :transporterId AND o.status = 'ACCEPTED'")
List<DispatchOffer> findAcceptedOffersByTransporter(@Param("transporterId") Long transporterId);

/**
* 기사의 ACCEPTED 상태 제안 개수 조회
*/
@Query("SELECT COUNT(o) FROM DispatchOffer o WHERE o.transporter.id = :transporterId AND o.status = 'ACCEPTED'")
int countAcceptedOffersByTransporter(@Param("transporterId") Long transporterId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,30 @@ public interface DispatchRepository extends JpaRepository<Dispatch, Long>,
d.toll_type as tollType
FROM dispatch d
WHERE d.active = true
AND (CAST(:statuses AS text[]) IS NULL OR d.status = ANY(CAST(:statuses AS text[])))
ORDER BY distanceInMeters ASC
""", nativeQuery = true)
List<DispatchDistanceProjection> findDispatchesByDistance(
List<DispatchDistanceProjection> findDispatchesByDistance(@Param("lat") double lat, @Param("lon") double lon);

@Query(value = """
SELECT d.id as id,
d.service as serviceType,
d.charge as charge,
d.start_location as startLocation,
d.destination_location as destinationLocation,
d.status as status,
ST_DistanceSphere(
ST_SetSRID(ST_MakePoint(d.start_longitude, d.start_latitude), 4326),
ST_SetSRID(ST_MakePoint(:lon, :lat), 4326)
) as distanceInMeters,
d.via_type as viaType,
d.payment_type as paymentType,
d.toll_type as tollType
FROM dispatch d
WHERE d.active = true
AND d.status IN (:statuses)
ORDER BY distanceInMeters ASC
""", nativeQuery = true)
List<DispatchDistanceProjection> findDispatchesByDistanceAndStatus(
@Param("lat") double lat,
@Param("lon") double lon,
@Param("statuses") List<String> statuses
Expand Down Expand Up @@ -95,4 +115,33 @@ Optional<Dispatch> findFirstByTransporterIdAndStatusOrderByAssignedAtDesc(
@Param("transporterId") Long transporterId,
@Param("status") StatusType status
);

/**
* 특정 기사의 기간별 완료된 배차 조회
* @param transporterId 기사 ID
* @param fromDate 시작일 (00:00:00)
* @param toDate 종료일 (23:59:59)
* @return 완료된 배차 리스트 (assignedAt 최신순)
*/
@Query("""
SELECT d FROM Dispatch d
WHERE d.transporter.id = :transporterId
AND d.status = 'COMPLETED'
AND d.completedAt >= :fromDate
AND d.completedAt <= :toDate
ORDER BY d.assignedAt DESC
""")
List<Dispatch> findCompletedDispatchesByTransporterIdAndDateRange(
@Param("transporterId") Long transporterId,
@Param("fromDate") java.time.LocalDateTime fromDate,
@Param("toDate") java.time.LocalDateTime toDate
);

/**
* 배차 상세 조회 (Transporter와 Fetch Join)
* @param dispatchId 배차 ID
* @return 배차 정보 (Transporter 포함)
*/
@Query("SELECT d FROM Dispatch d LEFT JOIN FETCH d.transporter WHERE d.id = :dispatchId")
Optional<Dispatch> findByIdWithTransporter(@Param("dispatchId") Long dispatchId);
}
Loading