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}