From c13bda5aae31567377db2d7b889f537fd33688f7 Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 3 Jul 2026 17:02:51 +0900 Subject: [PATCH] =?UTF-8?q?[2311]=20=EC=9D=B8=EC=A6=9D=EC=9A=A9=EB=9F=89?= =?UTF-8?q?=20=EB=8F=99=EC=9D=BCPCS=EB=8B=A4=EC=A4=91=EC=9C=A0=EB=8B=9B=20?= =?UTF-8?q?=EB=8C=80=EC=9D=91=20=E2=80=94=20CONN=5FVOL=5FW=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=C2=B7=EC=BF=BC=EB=A6=AC=C2=B7=EB=B0=B1=ED=95=84=20API?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../biz/estimate/EstimateController.java | 8 + .../qcast/biz/estimate/EstimateMapper.java | 3 + .../qcast/biz/estimate/EstimateService.java | 160 +++++++++++++++--- .../qcast/biz/estimate/dto/ItemRequest.java | 4 + .../mappers/estimate/estimateMapper.xml | 47 ++++- 5 files changed, 194 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/interplug/qcast/biz/estimate/EstimateController.java b/src/main/java/com/interplug/qcast/biz/estimate/EstimateController.java index 38978c47..9cc15e98 100644 --- a/src/main/java/com/interplug/qcast/biz/estimate/EstimateController.java +++ b/src/main/java/com/interplug/qcast/biz/estimate/EstimateController.java @@ -87,6 +87,14 @@ public class EstimateController { estimateService.updateEstimateLock(estimateRequest); } + // 2026-06-30: 회로 유닛별 접속용량(CONN_VOL_W)만 갱신 (기존 데이터 백필용) + @Operation(description = "견적서 회로 유닛별 접속용량(CONN_VOL_W)만 갱신한다.") + @PostMapping("/update-conn-vol") + @ResponseStatus(HttpStatus.OK) + public int updateEstimateConnVol(@RequestBody EstimateRequest estimateRequest) throws Exception { + return estimateService.updateEstimateConnVol(estimateRequest); + } + @Operation(description = "견적서를 엑셀로 다운로드한다.") @PostMapping("/excel-download") @ResponseStatus(HttpStatus.OK) diff --git a/src/main/java/com/interplug/qcast/biz/estimate/EstimateMapper.java b/src/main/java/com/interplug/qcast/biz/estimate/EstimateMapper.java index 2a5e56f3..8d9f48c6 100644 --- a/src/main/java/com/interplug/qcast/biz/estimate/EstimateMapper.java +++ b/src/main/java/com/interplug/qcast/biz/estimate/EstimateMapper.java @@ -93,6 +93,9 @@ public interface EstimateMapper { // 견적서 지붕면 회로구성 아이템 등록 public int insertEstimateCircuitItem(ItemRequest itemRequest); + // 2026-06-30: 회로 유닛별 접속용량(CONN_VOL_W)만 비파괴 갱신 (기존 데이터 백필용) + public int updateEstimateCircuitConnVol(ItemRequest itemRequest); + // 견적서 도면 아이템 등록 public int insertEstimateDrawingItem(ItemRequest itemRequest); diff --git a/src/main/java/com/interplug/qcast/biz/estimate/EstimateService.java b/src/main/java/com/interplug/qcast/biz/estimate/EstimateService.java index 0773d437..e7e47e6b 100644 --- a/src/main/java/com/interplug/qcast/biz/estimate/EstimateService.java +++ b/src/main/java/com/interplug/qcast/biz/estimate/EstimateService.java @@ -2870,6 +2870,61 @@ public class EstimateService { return cnt; } + /** + * 2026-06-30: 기존 견적서의 회로 유닛별 접속용량(CONN_VOL_W) 백필 전용 API. + * + *

전체 저장(insertEstimate) 없이 T_PART_CIRCUIT_ITEM_ESTIMATE 의 CONN_VOL_W 만 비파괴 UPDATE 한다. + * 프론트가 재구성한 모듈별 circuit("1-2")·specification 과 PCS 목록을 받아, 저장 시점과 동일한 + * 규칙(getPcsCircuitList)으로 유닛별 W 합을 계산해 CIRCUIT_NO 순서대로 갱신한다. + * 동일 PCS 모델 다중 유닛은 ITEM_ID 가 같아 ITEM_ID 로 못 나누므로, 저장 당시와 동일한 + * 순차 CIRCUIT_NO(빈 회로 스킵 포함)로 매칭한다. + * + * @param estimateRequest objectNo, planNo, roofSurfaceList(모듈), circuitItemList(PCS) 필요 + * @return 갱신된 회로 행 수 + */ + public int updateEstimateConnVol(EstimateRequest estimateRequest) throws Exception { + if (StringUtils.isEmpty(estimateRequest.getObjectNo())) { + throw new QcastException(ErrorCode.INVALID_INPUT_VALUE, + message.getMessage("common.message.required.data", "Object No")); + } + if (StringUtils.isEmpty(estimateRequest.getPlanNo())) { + throw new QcastException(ErrorCode.INVALID_INPUT_VALUE, + message.getMessage("common.message.required.data", "Plan No")); + } + + List circuitItemList = estimateRequest.getCircuitItemList(); + List roofSurfaceList = estimateRequest.getRoofSurfaceList(); + + List allModuleList = new ArrayList(); + if (roofSurfaceList != null) { + for (RoofRequest roofRequest : roofSurfaceList) { + if (roofRequest.getModuleList() != null) { + allModuleList.addAll(roofRequest.getModuleList()); + } + } + } + + // 저장 시점과 동일 규칙으로 유닛별 CONN_VOL_W(+CIRCUIT_CFG) 계산 + List circuitPcsItemList = this.getPcsCircuitList(circuitItemList, allModuleList); + + int updated = 0; + int circuitNo = 0; + if (circuitPcsItemList != null) { + for (ItemRequest circuitItemRequest : circuitPcsItemList) { + // insertEstimateCircuitItem 과 동일하게 빈 회로는 저장되지 않았으므로 CIRCUIT_NO 도 증가시키지 않음 + if (StringUtils.isEmpty(circuitItemRequest.getCircuitCfg())) { + continue; + } + circuitNo++; + circuitItemRequest.setObjectNo(estimateRequest.getObjectNo()); + circuitItemRequest.setPlanNo(estimateRequest.getPlanNo()); + circuitItemRequest.setCircuitNo(String.valueOf(circuitNo)); + updated += estimateMapper.updateEstimateCircuitConnVol(circuitItemRequest); + } + } + return updated; + } + /** * PC 회로 구성도 설정 * @@ -2880,32 +2935,67 @@ public class EstimateService { private List getPcsCircuitList(List pcsItemList, List moduleList) { - if (pcsItemList != null && pcsItemList.size() > 0) { + if (pcsItemList == null || pcsItemList.isEmpty()) { + return pcsItemList; + } - for (ItemRequest itemRequest : moduleList) { - itemRequest - .setCircuit(itemRequest.getCircuit().replaceAll("\\(", "").replaceAll("\\)", "")); - } - - if (pcsItemList.size() == 1) { - pcsItemList.get(0).setCircuitCfg(this.getPcsCircuitCtg(moduleList)); - } else { - if (moduleList != null && moduleList.size() > 0) { - int j = 1; - for (ItemRequest data : pcsItemList) { - String val = String.valueOf(j); - - List resultList = moduleList.stream() - .filter(t -> StringUtils.equals(val, t.getCircuit().split("-")[0])) - .collect(Collectors.toList()); - data.setCircuitCfg(this.getPcsCircuitCtg(resultList)); - j++; - } + // circuit 괄호 제거 (예: "(1-1)" -> "1-1") + if (moduleList != null) { + for (ItemRequest module : moduleList) { + if (module.getCircuit() != null) { + module.setCircuit(module.getCircuit().replaceAll("\\(", "").replaceAll("\\)", "")); } } } - return pcsItemList; + // 단일 PCS: 모든 모듈이 그 유닛에 접속 + if (pcsItemList.size() == 1) { + pcsItemList.get(0).setCircuitCfg(this.getPcsCircuitCtg(moduleList)); + // 2026-06-30: 유닛별 접속용량(W) 저장 — 동일 PCS 모델이 여러 유닛일 때 위치기반 추정으로 못 푸는 인증용량을 정확히 계산하기 위함 + pcsItemList.get(0).setConnVolW(this.sumModuleSpecW(moduleList)); + return pcsItemList; + } + + if (moduleList == null || moduleList.isEmpty()) { + return new ArrayList(); + } + + // 2026-06-30 보강: 다중 PCS 의 유닛 판별을 circuit 첫 숫자 기준으로. + // pcsItemList 개수(paralQty 로 과다 펼침 등)나 순서 흔들림에 비의존 — + // 실제 존재하는 유닛 index 만큼만 결과 행을 만들고, 유닛 u 의 PCS = pcsItemList[u-1] 로 매칭한다. + java.util.TreeSet unitKeys = new java.util.TreeSet(); + for (ItemRequest module : moduleList) { + String circuit = module.getCircuit(); + if (!StringUtils.isEmpty(circuit)) { + try { + unitKeys.add(Integer.parseInt(circuit.split("-")[0].trim())); + } catch (NumberFormatException e) { + // 숫자가 아닌 circuit 은 유닛 판별에서 제외 + } + } + } + + List resultPcsList = new ArrayList(); + for (Integer unit : unitKeys) { + String unitStr = String.valueOf(unit); + List unitModules = moduleList.stream() + .filter(t -> !StringUtils.isEmpty(t.getCircuit()) + && StringUtils.equals(unitStr, t.getCircuit().split("-")[0].trim())) + .collect(Collectors.toList()); + + // 유닛 index(1-based) 로 PCS 선택. 범위를 벗어나면 마지막 PCS 로 방어적 폴백 + int idx = unit - 1; + ItemRequest pcs = (idx >= 0 && idx < pcsItemList.size()) + ? pcsItemList.get(idx) + : pcsItemList.get(pcsItemList.size() - 1); + + pcs.setCircuitCfg(this.getPcsCircuitCtg(unitModules)); + // 2026-06-30: 이 유닛(circuit 첫 숫자=unit)에 배선된 모듈 W 합 = 접속용량 + pcs.setConnVolW(this.sumModuleSpecW(unitModules)); + resultPcsList.add(pcs); + } + + return resultPcsList; } /** @@ -2948,6 +3038,34 @@ public class EstimateService { return circuitCfg; } + /** + * 2026-06-30: PCS 유닛별 접속용량(W) 합산 + * + *

유닛에 배선된 모듈들의 specification(=모듈 W, 인증 쿼리가 CAST(SPECIFICATION AS FLOAT)로 쓰는 값) 합. + * CIRCUIT_CFG 는 회로별 '개수'만 남기고 모듈 타입(435/270 W)을 버리므로, 동일 PCS 모델이 여러 유닛일 때 + * 위치기반으로는 유닛별 접속용량을 복원할 수 없다. 그래서 모듈 W가 살아있는 저장 시점에 직접 합산해 보관한다. + * + * @param moduleList 한 PCS 유닛에 배선된 모듈 목록(모듈 1장 = 1 entry) + * @return W 합 문자열(모듈 없음이면 null) + */ + private String sumModuleSpecW(List moduleList) { + if (moduleList == null || moduleList.isEmpty()) { + return null; + } + java.math.BigDecimal sum = java.math.BigDecimal.ZERO; + for (ItemRequest module : moduleList) { + String spec = module.getSpecification(); + if (!StringUtils.isEmpty(spec)) { + try { + sum = sum.add(new java.math.BigDecimal(spec.trim())); + } catch (NumberFormatException e) { + // 숫자가 아닌 specification 은 접속용량 합산에서 제외 + } + } + } + return sum.toPlainString(); + } + public EstimateApiResponse selectAgencyCustList(PriceRequest priceRequest) throws Exception { // Validation diff --git a/src/main/java/com/interplug/qcast/biz/estimate/dto/ItemRequest.java b/src/main/java/com/interplug/qcast/biz/estimate/dto/ItemRequest.java index 8497725a..de9d95f4 100644 --- a/src/main/java/com/interplug/qcast/biz/estimate/dto/ItemRequest.java +++ b/src/main/java/com/interplug/qcast/biz/estimate/dto/ItemRequest.java @@ -92,6 +92,10 @@ public class ItemRequest { @Schema(description = "W") private String pnowW; + // 2026-06-30: PCS 유닛별 접속용량(W) — 저장 시점에 유닛에 배선된 모듈 W 합. 인증용량 계산 시 PNOW_W 와 MIN 비교에 사용 + @Schema(description = "PCS 유닛별 접속용량(W)") + private String connVolW; + @Schema(description = "아이템 그룹코드") private String itemGroup; diff --git a/src/main/resources/mappers/estimate/estimateMapper.xml b/src/main/resources/mappers/estimate/estimateMapper.xml index 3eb695a9..b41fa02f 100644 --- a/src/main/resources/mappers/estimate/estimateMapper.xml +++ b/src/main/resources/mappers/estimate/estimateMapper.xml @@ -407,6 +407,7 @@ SELECT cie.CIRCUIT_NO, cie.OBJECT_NO, cie.PLAN_NO, cie.ITEM_ID AS CIRCUIT_ITEM_ID, cie.CIRCUIT_CFG, + cie.CONN_VOL_W, -- 2026-06-30: 저장 시점 유닛별 접속용량(W). 있으면 위치기반 추정보다 우선 사용(동일 PCS 모델 다중 유닛 대응) (SELECT SUM(CAST(LTRIM(RTRIM(x.X.value('.','VARCHAR(10)'))) AS INT)) FROM (SELECT CAST(''+REPLACE(cie.CIRCUIT_CFG,',','')+'' AS XML) AS d) t CROSS APPLY t.d.nodes('/X') AS x(X) @@ -417,11 +418,14 @@ ), CC AS ( SELECT cs.*, + -- 2026-06-30: 누적 윈도우를 PCS(CIRCUIT_ITEM_ID) 내부로 한정 — 회로 위치가 다른 PCS 모듈까지 끌어오던 오배분 차단 ISNULL((SELECT SUM(c2.CIRCUIT_SUM) FROM CS c2 WHERE c2.OBJECT_NO=cs.OBJECT_NO AND c2.PLAN_NO=cs.PLAN_NO + AND c2.CIRCUIT_ITEM_ID = cs.CIRCUIT_ITEM_ID AND c2.CIRCUIT_NO < cs.CIRCUIT_NO), 0) AS CUM_START, ISNULL((SELECT SUM(c2.CIRCUIT_SUM) FROM CS c2 WHERE c2.OBJECT_NO=cs.OBJECT_NO AND c2.PLAN_NO=cs.PLAN_NO + AND c2.CIRCUIT_ITEM_ID = cs.CIRCUIT_ITEM_ID AND c2.CIRCUIT_NO <= cs.CIRCUIT_NO), 0) AS CUM_END FROM CS cs ), @@ -430,11 +434,14 @@ rie.ROOF_ITEM_NO, rie.ROOF_SURFACE_ID, rie.OBJECT_NO, rie.PLAN_NO, rie.ITEM_ID AS ROOF_ITEM_ID, rie.ITEM_NAME, CAST(rie.SPECIFICATION AS FLOAT) AS SPEC, rie.AMOUNT, rie.PC_ITEM_ID, + -- 2026-06-30: 모듈 누적도 PCS(PC_ITEM_ID) 내부로 한정 — 실제 배선된 PCS의 모듈만 누적 ISNULL((SELECT SUM(r2.AMOUNT) FROM T_PART_ROOF_ITEM_ESTIMATE r2 WHERE r2.OBJECT_NO=rie.OBJECT_NO AND r2.PLAN_NO=rie.PLAN_NO + AND r2.PC_ITEM_ID = rie.PC_ITEM_ID AND r2.ROOF_ITEM_NO < rie.ROOF_ITEM_NO), 0) AS CUM_START, ISNULL((SELECT SUM(r2.AMOUNT) FROM T_PART_ROOF_ITEM_ESTIMATE r2 WHERE r2.OBJECT_NO=rie.OBJECT_NO AND r2.PLAN_NO=rie.PLAN_NO + AND r2.PC_ITEM_ID = rie.PC_ITEM_ID AND r2.ROOF_ITEM_NO <= rie.ROOF_ITEM_NO), 0) AS CUM_END FROM T_PART_ROOF_ITEM_ESTIMATE rie WHERE rie.OBJECT_NO = #{objectNo} AND rie.PLAN_NO = #{planNo} @@ -447,7 +454,9 @@ CASE WHEN cc.CUM_END < rc.CUM_END THEN cc.CUM_END ELSE rc.CUM_END END - CASE WHEN cc.CUM_START > rc.CUM_START THEN cc.CUM_START ELSE rc.CUM_START END AS ALLOCATED_QTY FROM CC cc + -- 2026-06-30: 실제 배선(PCS) 일치 조건 추가 — 회로의 PCS 와 모듈이 배선된 PCS 가 같은 경우만 겹침 INNER JOIN RC rc ON cc.OBJECT_NO = rc.OBJECT_NO AND cc.PLAN_NO = rc.PLAN_NO + AND cc.CIRCUIT_ITEM_ID = rc.PC_ITEM_ID WHERE cc.CUM_START < rc.CUM_END AND cc.CUM_END > rc.CUM_START ), DETAIL AS ( @@ -458,17 +467,27 @@ (SELECT mi.PNOW_W FROM M_ITEM mi WHERE mi.ITEM_ID = a.CIRCUIT_ITEM_ID) AS PNOW_W FROM ALLOC a ), - CIRCUIT_AGG AS ( - SELECT - CIRCUIT_NO, CIRCUIT_ITEM_ID, CIRCUIT_CFG, CIRCUIT_SUM, - SUM(SPEC_X_QTY) AS SUM_SPEC_X_QTY, PNOW_W, - CASE WHEN SUM(SPEC_X_QTY) <= PNOW_W THEN SUM(SPEC_X_QTY) ELSE PNOW_W END AS MIN_VALUE + POS AS ( + -- 위치기반(폴백) 유닛별 접속용량 W 합 — CONN_VOL_W 가 없는 기존 데이터에만 사용 + SELECT CIRCUIT_NO, CIRCUIT_ITEM_ID, SUM(SPEC_X_QTY) AS POS_W FROM DETAIL - GROUP BY CIRCUIT_NO, CIRCUIT_ITEM_ID, CIRCUIT_CFG, CIRCUIT_SUM, PNOW_W + GROUP BY CIRCUIT_NO, CIRCUIT_ITEM_ID + ), + CIRCUIT_AGG AS ( + -- 2026-06-30 option B: 저장된 CONN_VOL_W 우선, 없으면(NULL=기존 데이터) 위치기반 POS_W 폴백. + -- CS(유닛 목록) 기준으로 LEFT JOIN 하므로 유닛별 1행 보장. 유닛별 MIN(접속용량, PNOW_W) 후 합산. + SELECT + cs.CIRCUIT_NO, cs.CIRCUIT_ITEM_ID, + COALESCE(cs.CONN_VOL_W, pos.POS_W, 0) AS CONN_W, + (SELECT mi.PNOW_W FROM M_ITEM mi WHERE mi.ITEM_ID = cs.CIRCUIT_ITEM_ID) AS PNOW_W + FROM CS cs + LEFT JOIN POS pos + ON pos.CIRCUIT_NO = cs.CIRCUIT_NO + AND pos.CIRCUIT_ITEM_ID = cs.CIRCUIT_ITEM_ID ) -- 2026-06-05: SQL 은 W 합만 DECIMAL 로 정확히 반환. kW 변환·3자리 절사(FLOOR)는 모듈용량(totVolKw)과 동일하게 Java BigDecimal 에서 수행 → FLOAT 나눗셈 잔여(5.6899999999999995 / 5.339) 원천 차단 - SELECT CAST(SUM(MIN_VALUE) AS DECIMAL(18,3)) AS CERT_VOL_W + SELECT CAST(SUM(CASE WHEN CONN_W <= PNOW_W THEN CONN_W ELSE PNOW_W END) AS DECIMAL(18,3)) AS CERT_VOL_W FROM CIRCUIT_AGG ]]> @@ -1178,15 +1197,27 @@ , PLAN_NO , ITEM_ID , CIRCUIT_CFG + , CONN_VOL_W /* 2026-06-30: 유닛별 접속용량(W) — 인증용량 MIN 비교용 */ ) VALUES ( #{circuitNo} , #{objectNo} , #{planNo} , #{itemId} , #{circuitCfg} + , #{connVolW} ) + + /* sqlid : com.interplug.qcast.biz.estimate.updateEstimateCircuitConnVol */ + /* 2026-06-30: 기존 데이터 백필용 — 회로 유닛별 접속용량(CONN_VOL_W)만 비파괴 UPDATE. 동일 PCS 모델 다중 유닛은 ITEM_ID 로 못 나누므로 CIRCUIT_NO 로 매칭 */ + UPDATE T_PART_CIRCUIT_ITEM_ESTIMATE + SET CONN_VOL_W = #{connVolW} + WHERE OBJECT_NO = #{objectNo} + AND PLAN_NO = #{planNo} + AND CIRCUIT_NO = #{circuitNo} + + /* sqlid : com.interplug.qcast.biz.estimate.deleteEstimateRoofList */ DELETE FROM T_PART_ROOF_ESTIMATE @@ -1445,6 +1476,7 @@ , PLAN_NO , ITEM_ID , CIRCUIT_CFG + , CONN_VOL_W /* 2026-06-30: 복제 시에도 유닛별 접속용량 보존(미보존 시 인증용량이 위치기반 폴백으로 떨어짐) */ ) SELECT PCIE.CIRCUIT_NO @@ -1452,6 +1484,7 @@ , #{copyPlanNo} AS PLAN_NO , PCIE.ITEM_ID , PCIE.CIRCUIT_CFG + , PCIE.CONN_VOL_W FROM T_PART_CIRCUIT_ITEM_ESTIMATE PCIE WITH (NOLOCK) WHERE PCIE.OBJECT_NO = #{objectNo} AND PCIE.PLAN_NO = #{planNo} -- 2.47.2