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 88a0a55..fd62947 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 @@ -138,4 +138,18 @@ private void validateOwner(Transporter transporter) { throw new GlobalException(ResultCode.FORBIDDEN); } } + + /** + * 노출 범위 토글 (자사 <-> 통합) + * @return 변경된 상태값 + */ + public CallType toggleExposure() { + if (this.call == CallType.INTERNAL) { + this.call = CallType.INTEGRATED; + } else { + this.call = CallType.INTERNAL; + } + return this.call; + } + } 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 b60988d..fc20a67 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 @@ -1,6 +1,7 @@ package com.mobility.api.domain.office.controller; import com.mobility.api.domain.dispatch.entity.Dispatch; +import com.mobility.api.domain.office.dto.request.CancelDispatchReq; 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; @@ -23,7 +24,9 @@ import lombok.RequiredArgsConstructor; import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -168,9 +171,11 @@ public CommonResponse> getDispatchFeed( @Operation(summary = "배차 등록", description = "") @RequestMapping(path = "/dispatch", method = RequestMethod.POST) public CommonResponse createDispatch( + @AuthenticationPrincipal PrincipalDetails user, @Valid @RequestBody CreateDispatchReq createDispatchReq ) { - officeService.saveDispatch(createDispatchReq); + + officeService.saveDispatch(createDispatchReq, user.getManager()); return CommonResponse.success(null); } @@ -186,6 +191,7 @@ public CommonResponse createDispatch( @Operation(summary = "배차 수정", description = "") @RequestMapping(path = "/dispatch/{dispatch_id}", method = RequestMethod.PATCH) public CommonResponse updateDispatch( + @AuthenticationPrincipal PrincipalDetails user, @PathVariable("dispatch_id") Long dispatchId, @RequestBody UpdateDispatchReq updateDispatchReq ) { @@ -203,9 +209,11 @@ public CommonResponse updateDispatch( @Operation(summary = "배차 취소 (삭제)", description = "") @RequestMapping(path = "/dispatch-cancel/{dispatch_id}", method = RequestMethod.POST) public CommonResponse cancelDispatch( - @PathVariable("dispatch_id") Long dispatchId + @AuthenticationPrincipal PrincipalDetails user, + @PathVariable("dispatch_id") Long dispatchId, + @RequestBody CancelDispatchReq req ) { - officeService.cancelDispatch(dispatchId); + officeService.cancelDispatch(dispatchId, req, user.getManager()); return CommonResponse.success(null); // FIXME return값 수정 } @@ -253,11 +261,25 @@ public CommonResponse createTransporter( */ @Operation(summary = "기사 리스트 조회", description = "") @RequestMapping(path = "/transporter", method = RequestMethod.GET) - public CommonResponse> getMyTransporters( - @AuthenticationPrincipal PrincipalDetails user + public CommonResponse> getMyTransporters( + @AuthenticationPrincipal PrincipalDetails user, + @RequestParam(required = false) String status, // 필터: 없을 수도 있음 + @RequestParam(defaultValue = "0") int page, // 페이지: 안 보내면 0 + @RequestParam(defaultValue = "20") int size // 크기: 안 보내면 20 ) { + + // 페이징 객체 생성 (최신순 정렬 예시: id 내림차순) + Pageable pageable = PageRequest.of(page, size, Sort.by("id").descending()); + // userDetails.getUsername() -> 로그인한 관리자의 ID - List result = officeService.getMyTransporters(user.getManager()); +// List result = officeService.getMyTransporters(user.getManager()); + + // 서비스 호출 + Page result = officeService.getMyTransporters( + user.getManager(), + status, + pageable + ); return CommonResponse.success(result); } @@ -271,7 +293,7 @@ public CommonResponse> getMyTransporters( */ @Operation(summary = "기사 상태 변경", description = "") @RequestMapping(path = "/transporter/{transporterId}/status", method = RequestMethod.PATCH) - public CommonResponse changeTransporterStatus( + public CommonResponse changeTransporterStatus( @AuthenticationPrincipal PrincipalDetails user, @PathVariable Long transporterId, @RequestBody TransporterStatusUpdateReq req @@ -283,7 +305,28 @@ public CommonResponse changeTransporterStatus( user.getManager() ); - return CommonResponse.success(0); + return CommonResponse.success("기사 상태 변경 성공"); + } + + /** + *
+     *     배차 노출범위 변경
+     * 
+ * @param user + * @return + */ + @Operation(summary = "배차 노출범위 변경", description = "") + @RequestMapping(path = "/dispatch/{dispatchId}/exposure", method = RequestMethod.PATCH) + public CommonResponse changeDispatchExposure( + @AuthenticationPrincipal PrincipalDetails user, + @PathVariable Long dispatchId + ) { + + // 서비스 호출 및 결과 받기 + String changedStatus = officeService.changeDispatchExposure(dispatchId, user.getManager()); + + // 변경된 상태를 메시지나 데이터로 주면 프론트에서 UI 갱신하기 편함 + return CommonResponse.success(changedStatus); } } diff --git a/src/main/java/com/mobility/api/domain/office/dto/request/CancelDispatchReq.java b/src/main/java/com/mobility/api/domain/office/dto/request/CancelDispatchReq.java new file mode 100644 index 0000000..f79d0bf --- /dev/null +++ b/src/main/java/com/mobility/api/domain/office/dto/request/CancelDispatchReq.java @@ -0,0 +1,6 @@ +package com.mobility.api.domain.office.dto.request; + +public record CancelDispatchReq( + String cancelReason // 취소 사유 +) { +} diff --git a/src/main/java/com/mobility/api/domain/office/dto/request/CreateDispatchReq.java b/src/main/java/com/mobility/api/domain/office/dto/request/CreateDispatchReq.java index 8ceac49..a7c07fa 100644 --- a/src/main/java/com/mobility/api/domain/office/dto/request/CreateDispatchReq.java +++ b/src/main/java/com/mobility/api/domain/office/dto/request/CreateDispatchReq.java @@ -1,9 +1,7 @@ package com.mobility.api.domain.office.dto.request; import com.mobility.api.domain.dispatch.entity.Dispatch; -import com.mobility.api.domain.dispatch.enums.CallType; -import com.mobility.api.domain.dispatch.enums.ServiceType; -import com.mobility.api.domain.dispatch.enums.StatusType; +import com.mobility.api.domain.dispatch.enums.*; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -46,20 +44,27 @@ public record CreateDispatchReq( String clientPhoneNumber, // 고객 전화번호 // StatusType status, // 배차 상태 - Long officeId, // FIXME 사무실 id, 토큰 등에서 받아오도록 변경해야 함 // CallType call, // 콜 타입 @Schema(description = "활성화 여부", example = "true") Boolean active, // 활성화 여부 - @Schema(description = "활성화 여부", example = "DELIVERY") + @Schema(description = "서비스 타입", example = "DELIVERY") ServiceType service, // 탁송 / 대리 - LocalDateTime createdAt // 생성 시간 + @Schema(description = "배차 번호", example = "2024-0001") + String dispatchNumber, // 배차 번호 + + @Schema(description = "메모", example = "memo") + String memo, // 메모 + + @Schema(description = "결제 방식", example = "CASH") + PaymentType paymentType, // 결제 방식 (현금 / 후불 / 완후) + + @Schema(description = "톨비 방식", example = "HIPASS") + TollType tollType // 톨비 방식 (톨포 / 톨별 / 하이패스) + ) { - public CreateDispatchReq { - officeId = 1L; // FIXME 임시로 사무실 id는 1로 고정 - } public Dispatch toEntity() { return Dispatch.builder() @@ -72,10 +77,13 @@ public Dispatch toEntity() { .charge(this.charge()) .clientPhoneNumber(this.clientPhoneNumber()) .status(StatusType.OPEN) - .officeId(this.officeId()) .call(CallType.INTERNAL) .active(this.active()) .service(this.service()) + .dispatchNumber(this.dispatchNumber) + .memo(this.memo) + .paymentType(this.paymentType) + .tollType(this.tollType) .createdAt(LocalDateTime.now()) .build(); } diff --git a/src/main/java/com/mobility/api/domain/office/dto/request/UpdateDispatchReq.java b/src/main/java/com/mobility/api/domain/office/dto/request/UpdateDispatchReq.java index 725efd5..852dd1e 100644 --- a/src/main/java/com/mobility/api/domain/office/dto/request/UpdateDispatchReq.java +++ b/src/main/java/com/mobility/api/domain/office/dto/request/UpdateDispatchReq.java @@ -1,8 +1,6 @@ package com.mobility.api.domain.office.dto.request; -import com.mobility.api.domain.dispatch.enums.CallType; -import com.mobility.api.domain.dispatch.enums.ServiceType; -import com.mobility.api.domain.dispatch.enums.StatusType; +import com.mobility.api.domain.dispatch.enums.*; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "배차 수정 request 객체") @@ -42,6 +40,19 @@ public record UpdateDispatchReq( Boolean active, @Schema(description = "서비스 타입", example = "DELIVERY") - ServiceType service -) { + ServiceType service, + + @Schema(description = "배차 번호", example = "2024-0001") + String dispatchNumber, + + @Schema(description = "메모", example = "memo") + String memo, + + @Schema(description = "결제 방식", example = "CASH") + PaymentType paymentType, + + @Schema(description = "톨비 방식", example = "HIPASS") + TollType tollType + + ) { } \ 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 f3a6ef4..0b3ce20 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 @@ -1,9 +1,11 @@ package com.mobility.api.domain.office.service; import com.mobility.api.domain.dispatch.entity.Dispatch; +import com.mobility.api.domain.dispatch.enums.CallType; 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.office.dto.request.CancelDispatchReq; 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; @@ -77,7 +79,7 @@ public Page findAllDispatch(DispatchSearchDto searchDto, Page } @Transactional - public void saveDispatch(CreateDispatchReq createDispatchReq) { + public void saveDispatch(CreateDispatchReq createDispatchReq, Manager manager) { // 1. 주변 1km 내 자동배차 ON 기사 존재 여부 확인 boolean hasEligibleDrivers = transporterRepository.existsEligibleDriversWithinRadius( createDispatchReq.startLatitude(), @@ -87,6 +89,12 @@ public void saveDispatch(CreateDispatchReq createDispatchReq) { // 2. 배차 엔티티 생성 Dispatch dispatch = createDispatchReq.toEntity(); + if(manager == null || manager.getOffice() == null) { + throw new GlobalException(ResultCode.NOT_FOUND_OFFICE); + } + + dispatch.setOfficeId(manager.getOffice().getId()); + // 3. 적격 기사 유무에 따라 상태 결정 if (hasEligibleDrivers) { // 주변에 자동배차 ON 기사가 있음 → HOLD 상태로 시작 @@ -129,7 +137,7 @@ public void updateDispatch(Long dispatchId, UpdateDispatchReq req) { } @Transactional - public void cancelDispatch(Long dispatchId) { // <- 메서드 이름도 delete -> cancel로 변경 + public void cancelDispatch(Long dispatchId, CancelDispatchReq req, Manager user) { // <- 메서드 이름도 delete -> cancel로 변경 // 엔티티 조회 Dispatch dispatch = dispatchRepository.findById(dispatchId) @@ -142,6 +150,7 @@ public void cancelDispatch(Long dispatchId) { // <- 메서드 이름도 delete - // TODO: dispatch.cancel() 같은 엔티티 메서드로 캡슐화 dispatch.setStatus(StatusType.CANCELED); + dispatch.setCancelReason(req.cancelReason()); dispatch.setCanceledAt(java.time.LocalDateTime.now()); // @Transactional이 변경 감지(Dirty Checking)로 UPDATE @@ -209,23 +218,42 @@ public void createTransporter(TransporterCreateReq req, Manager manager) { * @param manager 로그인한 직원 */ @Transactional(readOnly = true) // 조회 전용이므로 readOnly 권장 (성능 향상) - public List getMyTransporters(Manager manager) { - - // 1. 관리자(사장님) 찾기 + public Page getMyTransporters(Manager manager, String statusStr, Pageable pageable) { // 2. 소속 사무실 확인 Office office = manager.getOffice(); if (office == null) { - throw new GlobalException(ResultCode.FIXME_FAIL); + throw new GlobalException(ResultCode.NOT_FOUND_OFFICE); } - // 3. 해당 사무실의 기사 리스트 조회 - List transporters = transporterRepository.findAllByOffice(office); + Page transporterPage; + + // 1. status 파라미터가 있으면 -> 해당 상태로 필터링 + if (statusStr != null && !statusStr.isBlank()) { + try { + // 프론트에서 소문자로 줘도 대문자로 변환 (active -> ACTIVE) + TransporterStatus status = TransporterStatus.valueOf(statusStr.toUpperCase()); + transporterPage = transporterRepository.findAllByOfficeAndStatus(office, status, pageable); + } catch (IllegalArgumentException e) { + // 이상한 status 문자열이 들어오면 빈 페이지 리턴 or 에러 처리 (여기선 빈 페이지) + return Page.empty(pageable); + } + } + // 2. status 파라미터가 없으면 -> 전체 조회 + else { + transporterPage = transporterRepository.findAllByOffice(office, pageable); + } - // 4. Entity List -> DTO List 변환하여 반환 - return transporters.stream() - .map(TransporterRes::from) - .toList(); + // 3. Entity Page -> DTO Page 변환 + return transporterPage.map(TransporterRes::from); + +// // 3. 해당 사무실의 기사 리스트 조회 +// List transporters = transporterRepository.findAllByOffice(office); +// +// // 4. Entity List -> DTO List 변환하여 반환 +// return transporters.stream() +// .map(TransporterRes::from) +// .toList(); } @Transactional @@ -317,4 +345,26 @@ public List getDispatchFeed(Integer limit, Manager manager) { .toList(); } + /** + * 배차 노출 범위 변경 (Toggle) + */ + @Transactional + public String changeDispatchExposure(Long dispatchId, Manager manager) { + + // 1. 배차 조회 + Dispatch dispatch = dispatchRepository.findById(dispatchId) + .orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_DISPATCH)); + + // 2. 권한 검증 (내 사무실 배차인지) + if (!dispatch.getOfficeId().equals(manager.getOffice().getId())) { + throw new GlobalException(ResultCode.UNAUTHORIZED_ACCESS); + } + + // 3. 상태 토글 (Entity 메서드 호출) + CallType newType = dispatch.toggleExposure(); + + // 4. 변경된 상태 문자열 반환 ("INTEGRATED" 등) + return newType.name(); + } + } 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 85fe4fe..869e29b 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 @@ -1,8 +1,11 @@ package com.mobility.api.domain.transporter.repository; import com.mobility.api.domain.office.entity.Office; +import com.mobility.api.domain.transporter.TransporterStatus; import com.mobility.api.domain.transporter.dto.TransporterDistanceProjection; import com.mobility.api.domain.transporter.entity.Transporter; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -85,4 +88,10 @@ boolean existsEligibleDriversWithinRadius( // 특정 사무실에 소속된 기사 목록 조회 List findAllByOffice(Office office); + // 1. 상태 필터 없이 전체 조회 (페이징) + Page findAllByOffice(Office office, Pageable pageable); + + // 2. 상태 필터 적용 조회 (페이징) + Page findAllByOfficeAndStatus(Office office, TransporterStatus status, Pageable pageable); + }