Skip to content
Open
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: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok'

/* Database */
// runtimeOnly 'com.mysql:mysql-connector-j'
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
runtimeOnly 'com.mysql:mysql-connector-j'
// runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'

/* Swagger OpenAPI */
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.14'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;
import java.util.Optional;


@Mapper
public interface DeadLineMapper {
// 자재 코드로 LineMaterial 정보 조회
Optional<LineMaterialInfo> findLineMaterialByMaterialCode(@Param("materialCode") String materialCode);

// 자재 코드 목록으로 일괄 조회
List<LineMaterialInfo> findLineMaterialsByMaterialCodes(@Param("materialCodes") List<String> materialCodes);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ public class LineMaterialInfo {
private String productionLineName;
private String productionLineStatus;
private int dailyCapacity;
private String materialCode;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.werp.sero.deadline.query.dto.DeadLineQueryResponseDTO;
import com.werp.sero.deadline.query.dto.LineMaterialInfo;
import com.werp.sero.production.query.dao.PPValidateMapper;
import com.werp.sero.production.query.dto.ProductionPlanRawDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand All @@ -13,8 +14,9 @@
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
Expand All @@ -31,6 +33,8 @@ public class DeadLineQueryServiceImpl implements DeadLineQueryService {
@Override
@Transactional(readOnly = true)
public List<DeadLineQueryResponseDTO> calculateDeadLine(DeadLineQueryRequestDTO request) {
long startTime = System.currentTimeMillis();
int queryCount = 0; // 쿼리 수 카운트

List<DeadLineQueryResponseDTO> responses = new ArrayList<>(); // 품목별 응답

Expand Down Expand Up @@ -92,7 +96,41 @@ public List<DeadLineQueryResponseDTO> calculateDeadLine(DeadLineQueryRequestDTO
}

/* =========================
* 3. 품목별 시뮬레이션
* 3. 벌크 조회 (품목 루프 진입 전)
* ========================= */
// 3-1. 모든 자재 코드 수집
List<String> allMaterialCodes = request.getItems().stream()
.map(DeadLineQueryRequestDTO.ItemRequest::getMaterialCode)
.distinct()
.collect(Collectors.toList());

// 3-2. 자재 코드 → 라인 정보 벌크 조회
List<LineMaterialInfo> lineMaterials =
deadLineMapper.findLineMaterialsByMaterialCodes(allMaterialCodes);
queryCount++;
Map<String, LineMaterialInfo> infoMap = lineMaterials.stream()
.collect(Collectors.toMap(LineMaterialInfo::getMaterialCode, info -> info));

// 3-3. 라인 ID 수집 + 날짜 범위 계산
List<Integer> lineIds = lineMaterials.stream()
.map(LineMaterialInfo::getProductionLineId)
.distinct()
.collect(Collectors.toList());

LocalDate etaLimit = productionDeadlineDate.plusDays(ETA_SEARCH_LIMIT_DAYS);

// 3-4. 원시 생산계획 조회 → Java에서 일별 수량 계산
Map<Integer, Map<String, Integer>> qtyMap = new HashMap<>();
if (!lineIds.isEmpty()) {
List<ProductionPlanRawDTO> rawPlans = ppValidateMapper.selectPlannedPlansByRange(
lineIds, startDate.toString(), etaLimit.toString()
);
queryCount++;
qtyMap = buildDailyQtyMap(rawPlans, startDate, etaLimit);
}

/* =========================
* 4. 품목별 시뮬레이션 (메모리 조회)
* ========================= */
for (DeadLineQueryRequestDTO.ItemRequest item : request.getItems()) {

Expand All @@ -110,9 +148,7 @@ public List<DeadLineQueryResponseDTO> calculateDeadLine(DeadLineQueryRequestDTO
continue;
}

LineMaterialInfo info = deadLineMapper
.findLineMaterialByMaterialCode(materialCode)
.orElse(null);
LineMaterialInfo info = infoMap.get(materialCode);

if (info == null) {
responses.add(new DeadLineQueryResponseDTO(
Expand All @@ -139,47 +175,37 @@ public List<DeadLineQueryResponseDTO> calculateDeadLine(DeadLineQueryRequestDTO
continue;
}

Map<String, Integer> lineQtyMap = qtyMap.getOrDefault(lineId, Collections.emptyMap());
int remainingQty = orderQty;
LocalDate finishDate = null;

/* =========================
* 3-1. 희망 납기 충족 여부 판단
* 4-1. 희망 납기 충족 여부 판단
* ========================= */
for (LocalDate d = startDate;
!d.isAfter(productionDeadlineDate);
d = d.plusDays(1)) {

int plannedQty =
ppValidateMapper.sumDailyPlannedQty(lineId, d.toString());

int plannedQty = lineQtyMap.getOrDefault(d.toString(), 0);
int available = Math.max(0, dailyCapacity - plannedQty);
int used = Math.min(available, remainingQty);
remainingQty -= used;

log.info(
"[DEADLINE] material={}, line={}, date={}, daily={}, planned={}, available={}, remaining={}",
materialCode, lineId, d, dailyCapacity, plannedQty, available, remainingQty
);

if (remainingQty <= 0) {
finishDate = d;
break;
}
}

/* =========================
* 3-2. ETA 탐색 (희망 납기 실패 시)
* 4-2. ETA 탐색 (희망 납기 실패 시)
* ========================= */
if (finishDate == null) {
LocalDate etaLimit = productionDeadlineDate.plusDays(ETA_SEARCH_LIMIT_DAYS);

for (LocalDate d = productionDeadlineDate.plusDays(1);
!d.isAfter(etaLimit);
d = d.plusDays(1)) {

int plannedQty =
ppValidateMapper.sumDailyPlannedQty(lineId, d.toString());

int plannedQty = lineQtyMap.getOrDefault(d.toString(), 0);
int available = Math.max(0, dailyCapacity - plannedQty);
int used = Math.min(available, remainingQty);
remainingQty -= used;
Expand All @@ -192,7 +218,7 @@ public List<DeadLineQueryResponseDTO> calculateDeadLine(DeadLineQueryRequestDTO
}

/* =========================
* 4. 결과 정리
* 5. 결과 정리
* ========================= */
boolean deliverable =
finishDate != null && !finishDate.isAfter(productionDeadlineDate);
Expand Down Expand Up @@ -224,10 +250,39 @@ public List<DeadLineQueryResponseDTO> calculateDeadLine(DeadLineQueryRequestDTO
));
}

long elapsedTime = System.currentTimeMillis() - startTime;
log.info("[DEADLINE 성능] 품목 수: {}, 총 쿼리 수: {}, 실행 시간: {}ms",
request.getItems().size(), queryCount, elapsedTime);

return responses;
}


/**
* 원시 생산계획 목록 → lineId별, 날짜별 계획수량 Map 변환
* 기존 sumDailyPlannedQty와 동일한 계산 로직:
* CEILING(production_quantity / (DATEDIFF(end_date, start_date) + 1))
*/
private Map<Integer, Map<String, Integer>> buildDailyQtyMap(
List<ProductionPlanRawDTO> rawPlans, LocalDate rangeStart, LocalDate rangeEnd) {
Map<Integer, Map<String, Integer>> qtyMap = new HashMap<>();
for (ProductionPlanRawDTO plan : rawPlans) {
LocalDate planStart = LocalDate.parse(plan.getStartDate());
LocalDate planEnd = LocalDate.parse(plan.getEndDate());
int totalDays = (int) ChronoUnit.DAYS.between(planStart, planEnd) + 1;
int dailyQty = (int) Math.ceil((double) plan.getProductionQuantity() / totalDays);

LocalDate effectiveStart = planStart.isBefore(rangeStart) ? rangeStart : planStart;
LocalDate effectiveEnd = planEnd.isAfter(rangeEnd) ? rangeEnd : planEnd;

Map<String, Integer> lineMap = qtyMap.computeIfAbsent(plan.getLineId(), k -> new HashMap<>());
for (LocalDate d = effectiveStart; !d.isAfter(effectiveEnd); d = d.plusDays(1)) {
lineMap.merge(d.toString(), dailyQty, Integer::sum);
}
}
return qtyMap;
}

/**
* "yyyy-MM-dd HH:mm:ss" 같이 초가 포함된 값이 들어와도
* "yyyy-MM-dd HH:mm"로 안전하게 잘라서 파싱되도록 정규화
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,9 @@ List<PPMonthlyPlanResponseDTO> selectMonthlyPlans(
List<PPDailyPreviewResponseDTO> selectDailyPreview(String date);

PPDetailResponseDTO selectProductionPlanDetail(int ppId);

// getDailyLineSummary 전용: material JOIN 없이 라인만 조회 (중복 제거)
List<ProductionLineResponseDTO> selectDistinctProductionLines(
@Param("factoryId") Integer factoryId
);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.werp.sero.production.query.dao;

import com.werp.sero.production.query.dto.ProductionPlanRawDTO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface PPValidateMapper {
String selectMaterialCodeByPRItem(@Param("prItemId") int prItemId);
Expand All @@ -21,4 +24,11 @@ int sumDailyPlannedQty(
@Param("lineId") int lineId,
@Param("date") String date
);

// 기간 내 해당 라인들의 원시 생산계획 조회 (Java 연산용)
List<ProductionPlanRawDTO> selectPlannedPlansByRange(
@Param("lineIds") List<Integer> lineIds,
@Param("startDate") String startDate,
@Param("endDate") String endDate
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.werp.sero.production.query.dto;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class ProductionPlanRawDTO {
private int lineId;
private String startDate;
private String endDate;
private int productionQuantity;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import com.werp.sero.common.util.DateTimeUtils;
import com.werp.sero.production.command.application.dto.PPMonthlyPlanResponseDTO;
import com.werp.sero.production.command.domain.aggregate.ProductionLine;
import com.werp.sero.production.command.domain.repository.ProductionLineRepository;
import com.werp.sero.production.exception.ProductionRequestItemNotFoundException;
import com.werp.sero.production.query.dao.PPQueryMapper;
import com.werp.sero.production.query.dao.PPValidateMapper;
Expand All @@ -15,8 +13,11 @@

import java.time.LocalDate;
import java.time.YearMonth;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class PPQueryServiceImpl implements PPQueryService{
Expand Down Expand Up @@ -82,25 +83,46 @@ public List<PPDailyPreviewResponseDTO> getDailyPreview(String date) {
}

@Override
@Transactional(readOnly = true)
public List<DailyLineSummaryResponseDTO> getDailyLineSummary(String month, Integer factoryId) {
long startTime = System.currentTimeMillis();
int queryCount = 0;

YearMonth ym = YearMonth.parse(month);
LocalDate start = ym.atDay(1);
LocalDate end = ym.atEndOfMonth();

// 가동 가능한 라인 조회 (dailyCapacity 포함)
List<ProductionLineResponseDTO> lines = ppQueryMapper.selectProductionLines(factoryId);
List<DailyLineSummaryResponseDTO> result = new ArrayList<>();
// 1. 가동 가능한 라인 조회 (중복 없이)
List<ProductionLineResponseDTO> lines = ppQueryMapper.selectDistinctProductionLines(factoryId);
queryCount++;

if (lines.isEmpty()) {
return new ArrayList<>();
}

// 2. 라인 ID 수집
List<Integer> lineIds = lines.stream()
.map(ProductionLineResponseDTO::getLineId)
.collect(Collectors.toList());

// 3. 원시 생산계획 조회 → Java에서 일별 수량 계산
List<ProductionPlanRawDTO> rawPlans = ppValidateMapper.selectPlannedPlansByRange(
lineIds, start.toString(), end.toString()
);
queryCount++;

// 라인별, 날짜별 계획 수량 집계
// 4. Map 변환: lineId → (date → plannedQty)
Map<Integer, Map<String, Integer>> qtyMap = buildDailyQtyMap(rawPlans, start, end);

// 5. 결과 조립 (메모리 조회)
List<DailyLineSummaryResponseDTO> result = new ArrayList<>();
for (ProductionLineResponseDTO line : lines) {
Map<String, Integer> lineQtyMap = qtyMap.getOrDefault(line.getLineId(), Collections.emptyMap());
Map<Integer, Integer> dailyMap = new HashMap<>();

LocalDate date = start;
while (!date.isAfter(end)) {
int plannedQty = ppValidateMapper.sumDailyPlannedQty(
line.getLineId(),
date.toString() // yyyy-MM-dd
);
int plannedQty = lineQtyMap.getOrDefault(date.toString(), 0);
dailyMap.put(date.getDayOfMonth(), plannedQty);
date = date.plusDays(1);
}
Expand All @@ -115,11 +137,39 @@ public List<DailyLineSummaryResponseDTO> getDailyLineSummary(String month, Integ
);
}

long elapsedTime = System.currentTimeMillis() - startTime;
log.info("[DAILY-LINE-SUMMARY 성능] 라인 수: {}, 총 쿼리 수: {}, 실행 시간: {}ms",
lines.size(), queryCount, elapsedTime);

return result;
}

@Override
public PPDetailResponseDTO getProductionPlanDetail(int ppId) {
return ppQueryMapper.selectProductionPlanDetail(ppId);
}

private Map<Integer, Map<String, Integer>> buildDailyQtyMap(
List<ProductionPlanRawDTO> rawPlans,
LocalDate rangeStart,
LocalDate rangeEnd
) {
Map<Integer, Map<String, Integer>> qtyMap = new HashMap<>();

for (ProductionPlanRawDTO plan : rawPlans) {
LocalDate planStart = LocalDate.parse(plan.getStartDate());
LocalDate planEnd = LocalDate.parse(plan.getEndDate());
int totalDays = (int) ChronoUnit.DAYS.between(planStart, planEnd) + 1;
int dailyQty = (int) Math.ceil((double) plan.getProductionQuantity() / totalDays);

LocalDate effectiveStart = planStart.isBefore(rangeStart) ? rangeStart : planStart;
LocalDate effectiveEnd = planEnd.isAfter(rangeEnd) ? rangeEnd : planEnd;

Map<String, Integer> lineMap = qtyMap.computeIfAbsent(plan.getLineId(), k -> new HashMap<>());
for (LocalDate d = effectiveStart; !d.isAfter(effectiveEnd); d = d.plusDays(1)) {
lineMap.merge(d.toString(), dailyQty, Integer::sum);
}
}
return qtyMap;
}
}
Loading