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
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ dependencies {

//Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'


implementation 'org.springframework.kafka:spring-kafka'
implementation 'com.fasterxml.jackson.core:jackson-databind'
Comment thread
taemin3 marked this conversation as resolved.
}

tasks.named('test') {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/sampoom/purchase/PurchaseApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class PurchaseApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ public ResponseEntity<ApiResponse<PurchaseOrderResponseDto>> cancelOrder(
return ApiResponse.success(SuccessStatus.OK, purchaseService.cancelOrder(orderId));
}

@Operation(summary = "자재 주문 입고 처리", description = "주문된 자재를 입고 처리합니다.")
@PatchMapping("/{orderId}/receive")
public ResponseEntity<ApiResponse<PurchaseOrderResponseDto>> receiveOrder(
@PathVariable Long orderId) {
return ApiResponse.success(SuccessStatus.OK, purchaseService.receiveOrder(orderId));
}
Comment thread
taemin3 marked this conversation as resolved.

@Operation(summary = "자재 주문 삭제", description = "주문을 삭제합니다(소프트 삭제).")
@DeleteMapping("/{orderId}")
public ResponseEntity<ApiResponse<Void>> deleteOrder(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class PurchaseOrderItemDto {
private String unit;
private Long quantity;
private BigDecimal unitPrice;
private Integer leadTimeDays; // 자재 리드타임 (일 단위)

public static PurchaseOrderItemDto from(PurchaseOrderItem item) {
return PurchaseOrderItemDto.builder()
Expand All @@ -26,6 +27,7 @@ public static PurchaseOrderItemDto from(PurchaseOrderItem item) {
.unit(item.getUnit())
.quantity(item.getQuantity())
.unitPrice(item.getUnitPrice())
.leadTimeDays(item.getLeadTimeDays())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;

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

@Getter
Expand All @@ -18,8 +18,7 @@ public class PurchaseOrderRequestDto {
private Long factoryId;
private String factoryName;

@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate requiredAt;
private LocalDateTime requiredAt;

private String requesterName; // 요청자 이름 추가

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.sampoom.purchase.api.purchase.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.sampoom.purchase.api.purchase.entity.OrderStatus;
import com.sampoom.purchase.api.purchase.entity.PurchaseOrder;
import com.sampoom.purchase.api.purchase.entity.PurchaseOrderItem;
Expand All @@ -25,8 +24,9 @@ public class PurchaseOrderResponseDto {
private String orderCode;
private LocalDateTime orderAt;

@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate requiredAt; // 날짜만
private LocalDateTime requiredAt;

private LocalDateTime expectedDeliveryAt;

private Long factoryId;
private String factoryName;
Expand All @@ -43,6 +43,7 @@ public static PurchaseOrderResponseDto from(PurchaseOrder order, List<PurchaseOr
.orderCode(order.getCode())
.orderAt(order.getOrderAt())
.requiredAt(order.getRequiredAt())
.expectedDeliveryAt(order.getExpectedDeliveryAt())
.factoryId(order.getFactoryId())
.factoryName(order.getFactoryName())
.requesterName(order.getRequesterName())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
import org.hibernate.annotations.SQLRestriction;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

@Entity
@Getter
Expand All @@ -31,7 +31,8 @@ public class PurchaseOrder extends SoftDeleteEntity {
private LocalDateTime orderAt;
private LocalDateTime receivedAt;
private LocalDateTime canceledAt;
private LocalDate requiredAt;
private LocalDateTime requiredAt;
private LocalDateTime expectedDeliveryAt; // 예정일 (주문일 + 최대 리드타임)


@Enumerated(EnumType.STRING)
Expand All @@ -50,6 +51,9 @@ public class PurchaseOrder extends SoftDeleteEntity {
@Column(precision = 19, scale = 2)
private BigDecimal expectedAmount; // 예상 금액

@OneToMany(mappedBy = "purchaseOrder", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<PurchaseOrderItem> items;

public void receive() {
if (this.status != OrderStatus.ORDERED) {
throw new BadRequestException(ErrorStatus.ORDER_ALREADY_PROCESSED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,6 @@ public class PurchaseOrderItem {

@Column(precision = 19, scale = 2)
private BigDecimal unitPrice;

private Integer leadTimeDays; // 자재 리드타임 (일 단위)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.sampoom.purchase.api.purchase.entity.*;
import com.sampoom.purchase.api.purchase.repository.PurchaseOrderItemRepository;
import com.sampoom.purchase.api.purchase.repository.PurchaseOrderRepository;
import com.sampoom.purchase.common.event.PurchaseEventService;
import com.sampoom.purchase.common.exception.NotFoundException;
import com.sampoom.purchase.common.response.ErrorStatus;
import com.sampoom.purchase.common.response.PageResponseDto;
Expand All @@ -30,6 +31,7 @@ public class PurchaseService {

private final PurchaseOrderRepository orderRepository;
private final PurchaseOrderItemRepository orderItemRepository;
private final PurchaseEventService purchaseEventService;

@Transactional
public PurchaseOrderResponseDto createMaterialOrder(PurchaseOrderRequestDto requestDto) {
Expand All @@ -43,39 +45,75 @@ public PurchaseOrderResponseDto createMaterialOrder(PurchaseOrderRequestDto requ
// 긴급도 계산: 오늘과 필요일 차이 기준
UrgencyLevel urgency = calculateUrgency(requestDto.getRequiredAt());

// 최대 리드타임 계산: 자재들 중 가장 긴 리드타임
Integer maxLeadTime = requestDto.getItems() == null ? 0 :
requestDto.getItems().stream()
.mapToInt(item -> item.getLeadTimeDays() == null ? 0 : item.getLeadTimeDays())
.max()
.orElse(0);

// 예정일 계산: 주문일 + 최대 리드타임
LocalDateTime expectedDeliveryAt = LocalDateTime.now().plusDays(maxLeadTime);

PurchaseOrder order = PurchaseOrder.builder()
.code(generateOrderCode())
.factoryId(requestDto.getFactoryId())
.status(OrderStatus.ORDERED)
.orderAt(LocalDateTime.now())
.factoryName(requestDto.getFactoryName())
.requiredAt(requestDto.getRequiredAt())
.expectedDeliveryAt(expectedDeliveryAt)
.requesterName(requestDto.getRequesterName())
.expectedAmount(expectedAmount)
.urgency(urgency)
.build();

orderRepository.save(order);

// lambda에서 사용하기 위해 final 변수로 복사
final PurchaseOrder savedOrder = order;

List<PurchaseOrderItem> orderItems = requestDto.getItems().stream()
.map(itemDto -> PurchaseOrderItem.builder()
.purchaseOrder(order)
.purchaseOrder(savedOrder)
.materialCode(itemDto.getMaterialCode())
.materialName(itemDto.getMaterialName())
.unit(itemDto.getUnit())
.quantity(itemDto.getQuantity())
.unitPrice(itemDto.getUnitPrice())
.leadTimeDays(itemDto.getLeadTimeDays())
.build())
.collect(Collectors.toList());

orderItemRepository.saveAll(orderItems);

return PurchaseOrderResponseDto.from(order, orderItems);
// order에 items 설정 (이벤트에서 사용하기 위해)
PurchaseOrder orderWithItems = PurchaseOrder.builder()
.id(savedOrder.getId())
.code(savedOrder.getCode())
.factoryId(savedOrder.getFactoryId())
.factoryName(savedOrder.getFactoryName())
.status(savedOrder.getStatus())
.orderAt(savedOrder.getOrderAt())
.receivedAt(savedOrder.getReceivedAt())
.canceledAt(savedOrder.getCanceledAt())
.requiredAt(savedOrder.getRequiredAt())
.expectedDeliveryAt(savedOrder.getExpectedDeliveryAt())
.requesterName(savedOrder.getRequesterName())
.expectedAmount(savedOrder.getExpectedAmount())
.urgency(savedOrder.getUrgency())
.items(orderItems)
.build();
Comment thread
taemin3 marked this conversation as resolved.

// 주문 생성 이벤트 발행
purchaseEventService.recordOrderCreated(orderWithItems);

return PurchaseOrderResponseDto.from(savedOrder, orderItems);
}

private UrgencyLevel calculateUrgency(LocalDate requiredAt) {
private UrgencyLevel calculateUrgency(LocalDateTime requiredAt) {
if (requiredAt == null) return UrgencyLevel.LOW;
long days = ChronoUnit.DAYS.between(LocalDate.now(), requiredAt);
long days = ChronoUnit.DAYS.between(LocalDateTime.now(), requiredAt);
if (days <= 1) return UrgencyLevel.HIGH;
if (days <= 3) return UrgencyLevel.MEDIUM;
return UrgencyLevel.LOW;
Expand Down Expand Up @@ -119,20 +157,103 @@ public PurchaseOrderResponseDto cancelOrder(Long orderId) {
.orElseThrow(() -> new NotFoundException(ErrorStatus.ORDER_NOT_FOUND));
order.cancel();
orderRepository.save(order);

// items 로드하여 이벤트에 포함
List<PurchaseOrderItem> items = orderItemRepository.findByPurchaseOrderId(orderId);

// order에 items 설정
order = PurchaseOrder.builder()
.id(order.getId())
.code(order.getCode())
.factoryId(order.getFactoryId())
.factoryName(order.getFactoryName())
.status(order.getStatus())
.orderAt(order.getOrderAt())
.receivedAt(order.getReceivedAt())
.canceledAt(order.getCanceledAt())
.requiredAt(order.getRequiredAt())
.expectedDeliveryAt(order.getExpectedDeliveryAt())
.requesterName(order.getRequesterName())
.expectedAmount(order.getExpectedAmount())
.urgency(order.getUrgency())
.items(items)
.build();

// 주문 취소 이벤트 발행
purchaseEventService.recordOrderCanceled(order);

return PurchaseOrderResponseDto.from(order, items);
}

@Transactional
public void deleteOrder(Long orderId) {
PurchaseOrder order = orderRepository.findById(orderId)
.orElseThrow(() -> new NotFoundException(ErrorStatus.ORDER_NOT_FOUND));

// items 로드하여 이벤트에 포함
List<PurchaseOrderItem> items = orderItemRepository.findByPurchaseOrderId(orderId);

// order에 items 설정
order = PurchaseOrder.builder()
.id(order.getId())
.code(order.getCode())
.factoryId(order.getFactoryId())
.factoryName(order.getFactoryName())
.status(order.getStatus())
.orderAt(order.getOrderAt())
.receivedAt(order.getReceivedAt())
.canceledAt(order.getCanceledAt())
.requiredAt(order.getRequiredAt())
.expectedDeliveryAt(order.getExpectedDeliveryAt())
.requesterName(order.getRequesterName())
.expectedAmount(order.getExpectedAmount())
.urgency(order.getUrgency())
.items(items)
.build();

// 주문 삭제 이벤트 발행
purchaseEventService.recordOrderDeleted(order);

orderRepository.delete(order);
}

@Transactional
public PurchaseOrderResponseDto receiveOrder(Long orderId) {
PurchaseOrder order = orderRepository.findById(orderId)
.orElseThrow(() -> new NotFoundException(ErrorStatus.ORDER_NOT_FOUND));
order.receive();
orderRepository.save(order);

// items 로드하여 이벤트에 포함
List<PurchaseOrderItem> items = orderItemRepository.findByPurchaseOrderId(orderId);

// order에 items 설정
order = PurchaseOrder.builder()
.id(order.getId())
.code(order.getCode())
.factoryId(order.getFactoryId())
.factoryName(order.getFactoryName())
.status(order.getStatus())
.orderAt(order.getOrderAt())
.receivedAt(order.getReceivedAt())
.canceledAt(order.getCanceledAt())
.requiredAt(order.getRequiredAt())
.expectedDeliveryAt(order.getExpectedDeliveryAt())
.requesterName(order.getRequesterName())
.expectedAmount(order.getExpectedAmount())
.urgency(order.getUrgency())
.items(items)
.build();

// 자재 입고 처리 이벤트 발행
purchaseEventService.recordOrderReceived(order);

return PurchaseOrderResponseDto.from(order, items);
}

private String generateOrderCode() {
String datePart = java.time.LocalDate.now()
.format(java.time.format.DateTimeFormatter.BASIC_ISO_DATE); // YYYYMMDD
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy")); // YYYY만 사용
String prefix = "ORD-" + datePart + "-";

String lastCode = orderRepository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class SwaggerConfig {
@Bean
public OpenAPI openAPI() {
Server localServer = new Server()
.url("http://localhost:8080/")
.url("http://localhost:8081/")
.description("로컬 서버");

Server prodServer = new Server()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.sampoom.purchase.common.event;

public enum OutboxStatus {
READY,
PUBLISHED,
FAILED,
DEAD
}
29 changes: 29 additions & 0 deletions src/main/java/com/sampoom/purchase/common/event/PurchaseEvent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.sampoom.purchase.common.event;

import java.util.List;

public record PurchaseEvent(
String eventId,
String eventType,
Long version,
String occurredAt,
Payload payload
) {
public record Payload(
Long orderId,
String orderCode,
Long factoryId,
String factoryName,
String status,
String receivedAt,
Boolean deleted,
List<Material> materials
) {}

public record Material(
String materialCode,
String materialName,
Integer quantity,
String unit
) {}
}
Loading