diff --git a/build.gradle b/build.gradle index d3675823..9688dd6b 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/src/main/java/com/werp/sero/deadline/query/dao/DeadLineMapper.java b/src/main/java/com/werp/sero/deadline/query/dao/DeadLineMapper.java index f2c975aa..ee1b3884 100644 --- a/src/main/java/com/werp/sero/deadline/query/dao/DeadLineMapper.java +++ b/src/main/java/com/werp/sero/deadline/query/dao/DeadLineMapper.java @@ -4,6 +4,7 @@ import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +import java.util.List; import java.util.Optional; @@ -11,4 +12,7 @@ public interface DeadLineMapper { // 자재 코드로 LineMaterial 정보 조회 Optional findLineMaterialByMaterialCode(@Param("materialCode") String materialCode); + + // 자재 코드 목록으로 일괄 조회 + List findLineMaterialsByMaterialCodes(@Param("materialCodes") List materialCodes); } diff --git a/src/main/java/com/werp/sero/deadline/query/dto/LineMaterialInfo.java b/src/main/java/com/werp/sero/deadline/query/dto/LineMaterialInfo.java index 06a57c06..e5a11b80 100644 --- a/src/main/java/com/werp/sero/deadline/query/dto/LineMaterialInfo.java +++ b/src/main/java/com/werp/sero/deadline/query/dto/LineMaterialInfo.java @@ -13,4 +13,5 @@ public class LineMaterialInfo { private String productionLineName; private String productionLineStatus; private int dailyCapacity; + private String materialCode; } diff --git a/src/main/java/com/werp/sero/deadline/query/service/DeadLineQueryServiceImpl.java b/src/main/java/com/werp/sero/deadline/query/service/DeadLineQueryServiceImpl.java index fcd8ec6c..e340b87d 100644 --- a/src/main/java/com/werp/sero/deadline/query/service/DeadLineQueryServiceImpl.java +++ b/src/main/java/com/werp/sero/deadline/query/service/DeadLineQueryServiceImpl.java @@ -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; @@ -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 @@ -31,6 +33,8 @@ public class DeadLineQueryServiceImpl implements DeadLineQueryService { @Override @Transactional(readOnly = true) public List calculateDeadLine(DeadLineQueryRequestDTO request) { + long startTime = System.currentTimeMillis(); + int queryCount = 0; // 쿼리 수 카운트 List responses = new ArrayList<>(); // 품목별 응답 @@ -92,7 +96,41 @@ public List calculateDeadLine(DeadLineQueryRequestDTO } /* ========================= - * 3. 품목별 시뮬레이션 + * 3. 벌크 조회 (품목 루프 진입 전) + * ========================= */ + // 3-1. 모든 자재 코드 수집 + List allMaterialCodes = request.getItems().stream() + .map(DeadLineQueryRequestDTO.ItemRequest::getMaterialCode) + .distinct() + .collect(Collectors.toList()); + + // 3-2. 자재 코드 → 라인 정보 벌크 조회 + List lineMaterials = + deadLineMapper.findLineMaterialsByMaterialCodes(allMaterialCodes); + queryCount++; + Map infoMap = lineMaterials.stream() + .collect(Collectors.toMap(LineMaterialInfo::getMaterialCode, info -> info)); + + // 3-3. 라인 ID 수집 + 날짜 범위 계산 + List lineIds = lineMaterials.stream() + .map(LineMaterialInfo::getProductionLineId) + .distinct() + .collect(Collectors.toList()); + + LocalDate etaLimit = productionDeadlineDate.plusDays(ETA_SEARCH_LIMIT_DAYS); + + // 3-4. 원시 생산계획 조회 → Java에서 일별 수량 계산 + Map> qtyMap = new HashMap<>(); + if (!lineIds.isEmpty()) { + List rawPlans = ppValidateMapper.selectPlannedPlansByRange( + lineIds, startDate.toString(), etaLimit.toString() + ); + queryCount++; + qtyMap = buildDailyQtyMap(rawPlans, startDate, etaLimit); + } + + /* ========================= + * 4. 품목별 시뮬레이션 (메모리 조회) * ========================= */ for (DeadLineQueryRequestDTO.ItemRequest item : request.getItems()) { @@ -110,9 +148,7 @@ public List calculateDeadLine(DeadLineQueryRequestDTO continue; } - LineMaterialInfo info = deadLineMapper - .findLineMaterialByMaterialCode(materialCode) - .orElse(null); + LineMaterialInfo info = infoMap.get(materialCode); if (info == null) { responses.add(new DeadLineQueryResponseDTO( @@ -139,28 +175,22 @@ public List calculateDeadLine(DeadLineQueryRequestDTO continue; } + Map 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; @@ -168,18 +198,14 @@ public List calculateDeadLine(DeadLineQueryRequestDTO } /* ========================= - * 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; @@ -192,7 +218,7 @@ public List calculateDeadLine(DeadLineQueryRequestDTO } /* ========================= - * 4. 결과 정리 + * 5. 결과 정리 * ========================= */ boolean deliverable = finishDate != null && !finishDate.isAfter(productionDeadlineDate); @@ -224,10 +250,39 @@ public List 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> buildDailyQtyMap( + List rawPlans, LocalDate rangeStart, LocalDate rangeEnd) { + Map> 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 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"로 안전하게 잘라서 파싱되도록 정규화 diff --git a/src/main/java/com/werp/sero/production/query/dao/PPQueryMapper.java b/src/main/java/com/werp/sero/production/query/dao/PPQueryMapper.java index 57effac8..88d66d16 100644 --- a/src/main/java/com/werp/sero/production/query/dao/PPQueryMapper.java +++ b/src/main/java/com/werp/sero/production/query/dao/PPQueryMapper.java @@ -32,4 +32,9 @@ List selectMonthlyPlans( List selectDailyPreview(String date); PPDetailResponseDTO selectProductionPlanDetail(int ppId); + + // getDailyLineSummary 전용: material JOIN 없이 라인만 조회 (중복 제거) + List selectDistinctProductionLines( + @Param("factoryId") Integer factoryId + ); } diff --git a/src/main/java/com/werp/sero/production/query/dao/PPValidateMapper.java b/src/main/java/com/werp/sero/production/query/dao/PPValidateMapper.java index 28a14725..bf2be7ae 100644 --- a/src/main/java/com/werp/sero/production/query/dao/PPValidateMapper.java +++ b/src/main/java/com/werp/sero/production/query/dao/PPValidateMapper.java @@ -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); @@ -21,4 +24,11 @@ int sumDailyPlannedQty( @Param("lineId") int lineId, @Param("date") String date ); + + // 기간 내 해당 라인들의 원시 생산계획 조회 (Java 연산용) + List selectPlannedPlansByRange( + @Param("lineIds") List lineIds, + @Param("startDate") String startDate, + @Param("endDate") String endDate + ); } diff --git a/src/main/java/com/werp/sero/production/query/dto/ProductionPlanRawDTO.java b/src/main/java/com/werp/sero/production/query/dto/ProductionPlanRawDTO.java new file mode 100644 index 00000000..017e456a --- /dev/null +++ b/src/main/java/com/werp/sero/production/query/dto/ProductionPlanRawDTO.java @@ -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; +} diff --git a/src/main/java/com/werp/sero/production/query/service/PPQueryServiceImpl.java b/src/main/java/com/werp/sero/production/query/service/PPQueryServiceImpl.java index 5a306d09..1f1776e3 100644 --- a/src/main/java/com/werp/sero/production/query/service/PPQueryServiceImpl.java +++ b/src/main/java/com/werp/sero/production/query/service/PPQueryServiceImpl.java @@ -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; @@ -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{ @@ -82,25 +83,46 @@ public List getDailyPreview(String date) { } @Override + @Transactional(readOnly = true) public List 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 lines = ppQueryMapper.selectProductionLines(factoryId); - List result = new ArrayList<>(); + // 1. 가동 가능한 라인 조회 (중복 없이) + List lines = ppQueryMapper.selectDistinctProductionLines(factoryId); + queryCount++; + + if (lines.isEmpty()) { + return new ArrayList<>(); + } + + // 2. 라인 ID 수집 + List lineIds = lines.stream() + .map(ProductionLineResponseDTO::getLineId) + .collect(Collectors.toList()); + + // 3. 원시 생산계획 조회 → Java에서 일별 수량 계산 + List rawPlans = ppValidateMapper.selectPlannedPlansByRange( + lineIds, start.toString(), end.toString() + ); + queryCount++; - // 라인별, 날짜별 계획 수량 집계 + // 4. Map 변환: lineId → (date → plannedQty) + Map> qtyMap = buildDailyQtyMap(rawPlans, start, end); + + // 5. 결과 조립 (메모리 조회) + List result = new ArrayList<>(); for (ProductionLineResponseDTO line : lines) { + Map lineQtyMap = qtyMap.getOrDefault(line.getLineId(), Collections.emptyMap()); Map 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); } @@ -115,6 +137,10 @@ public List getDailyLineSummary(String month, Integ ); } + long elapsedTime = System.currentTimeMillis() - startTime; + log.info("[DAILY-LINE-SUMMARY 성능] 라인 수: {}, 총 쿼리 수: {}, 실행 시간: {}ms", + lines.size(), queryCount, elapsedTime); + return result; } @@ -122,4 +148,28 @@ public List getDailyLineSummary(String month, Integ public PPDetailResponseDTO getProductionPlanDetail(int ppId) { return ppQueryMapper.selectProductionPlanDetail(ppId); } + + private Map> buildDailyQtyMap( + List rawPlans, + LocalDate rangeStart, + LocalDate rangeEnd + ) { + Map> 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 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; + } } diff --git a/src/main/resources/com/werp/sero/deadline/query/dao/DeadLineMapper.xml b/src/main/resources/com/werp/sero/deadline/query/dao/DeadLineMapper.xml index 47891ab2..0a1732ad 100644 --- a/src/main/resources/com/werp/sero/deadline/query/dao/DeadLineMapper.xml +++ b/src/main/resources/com/werp/sero/deadline/query/dao/DeadLineMapper.xml @@ -11,6 +11,7 @@ + @@ -27,5 +28,32 @@ LIMIT 1 + + diff --git a/src/main/resources/com/werp/sero/production/query/dao/PPQueryMapper.xml b/src/main/resources/com/werp/sero/production/query/dao/PPQueryMapper.xml index 523958f9..6f62a475 100644 --- a/src/main/resources/com/werp/sero/production/query/dao/PPQueryMapper.xml +++ b/src/main/resources/com/werp/sero/production/query/dao/PPQueryMapper.xml @@ -302,5 +302,25 @@ WHERE A.id = #{ppId} + + + + \ No newline at end of file diff --git a/src/main/resources/com/werp/sero/production/query/dao/PPValidateMapper.xml b/src/main/resources/com/werp/sero/production/query/dao/PPValidateMapper.xml index caf289c8..b84719a0 100644 --- a/src/main/resources/com/werp/sero/production/query/dao/PPValidateMapper.xml +++ b/src/main/resources/com/werp/sero/production/query/dao/PPValidateMapper.xml @@ -79,5 +79,26 @@ + + + + \ No newline at end of file