Merge remote-tracking branch 'origin/CODEX' into sub_laptop_8
This commit is contained in:
+91
-14
@@ -1,26 +1,95 @@
|
||||
- [x] 산출근거 주석 지정/해제(`popMainSetStr`) 및 블럭 삭제(`popMainBlockDel`) 구현
|
||||
- [x] 산출근거 주석 지정/해제(`popMainSetStr`), 블럭 삭제(`popMainBlockDel`), 공사비역산 맞추기(`popupTotal`), 레벨 일괄 증감(`mnuLevelUp/Down`), 설계변경 초기화(`mnuChangeInit`), 원가계산서 초기화(`popupInit`), 기성 회차 입력 초기화(`mnuFileAccomInit`), 연차공사 배분 초기화(`TFRMANUAL popInit`) 및 물가변동 입력 초기화(`TFRMESCALATION popInit`) 구현
|
||||
- 자체 검증:
|
||||
1) 산출근거 주석 지정/해제 (`popMainSetStr`, 0xf08b1c):
|
||||
1) 산출근거 주석 지정/해제 (`popMainSetStr`, 0xf08b1c) 및 음수 비목/Index 유형 보존:
|
||||
- REA `TFRMCOST`/`TFRMESTBASE`의 팝업 메뉴 `popMainSetStr` / `mnuSetGuSuc` ("주석으로 지정", Ctrl+J, 0xf08b1c) 스펙 근거 기반 구현.
|
||||
- 단가산출(`CostMainDat`) 화면의 산출근거 행에서 `Index`를 `-1`(주석)로 지정하거나 원래 `Index`(0)로 복원.
|
||||
- 주석 지정 시 `_origIndex: curIndex`를 명시적으로 보존하고, `Index === -1`로 전환. 주석 해제 시 `_origIndex` 또는 0으로 안전하게 복원하고 임시 속성 제거.
|
||||
- 호표 제목 행(`Index > 0`) 또는 소계 구분선(`Index === -20`)은 주석으로 지정할 수 없도록 안전 가드 적용.
|
||||
- 다중 선택 시 제목 행이나 비산출근거 행이 포함되면 전체 변경을 중단하고 원자성(Atomic) 보장.
|
||||
- 단가산출(`CostMainDat`) 화면의 산출근거 행에서 `Index`를 `-1`(주석)로 지정하거나 원래 유형으로 복원.
|
||||
- **음수 유형 및 비목 행(-2 재료비, -3 경비, -5 소계) 온전한 보존·복원**:
|
||||
- 주석 지정 시 `_origIndex: curIndex`뿐만 아니라 `_origSCheck: curSCheck`를 명시적으로 보존하고 `Index = -1`, `SCheck = -1`로 전환.
|
||||
- 주석 해제 시 `_origIndex`(또는 스냅샷 `Index_`)와 `_origSCheck`(또는 `SCheck_`, `OCheck`)를 통해 원래 비목 유형(-2, -3, -5 등)과 `Index`로 100% 정밀 복원 후 임시 속성 제거.
|
||||
- 호표 제목 행(`Index > 0`) 또는 소계 구분선(`Index === -20`)은 주석으로 지정할 수 없도록 안전 가드 적용.
|
||||
- 다중 선택 시 제목 행이나 비산출근거 행이 포함되면 전체 변경을 중단하고 원자성(Atomic) 보장.
|
||||
- **저장 및 재열기 왕복 검증**: JSON 직렬화/역직렬화 후에도 `_origIndex`, `_origSCheck` 및 원래 비목 정보가 보존되어 온전하게 복원됨을 검증.
|
||||
- `native-text.js`에서 `Index === -1`인 행은 소계(`LinkIndex: 61`) 계산 시 `if (num(previous.Index) !== -1)` 조건에 따라 자동으로 합산에서 제외되므로 산출식 수치 무결성 완벽 보장.
|
||||
2) 블럭 삭제 (`popMainBlockDel`, 0xf08adc):
|
||||
2) 블럭 삭제 (`popMainBlockDel`, 0xf08adc) 및 실제 파일 참조 보호:
|
||||
- REA `TFRMCOST`/`TFRMHEAD`/`TFRMMACH`/`TFRMEST` 전역의 `popMainBlockDel` ("블럭삭제", Ctrl+Del, 0xf08adc) 스펙 근거 기반 구현.
|
||||
- `EstBlockActions.deleteBlockRows(state)`: 선택된 범위/블럭의 행들을 일괄 삭제.
|
||||
- 호표 제목 행(`Index > 0`)이 포함된 경우 호표 전체 행을 일괄 삭제 범위로 확장.
|
||||
- 다른 항목(`EstMainDat`, `HeadMainDat` 등)에서 참조 중인 코드가 포함된 경우 "다른 항목에서 사용 중입니다. 연결을 먼저 해제하세요."로 안전 차단.
|
||||
- **참조 보호 검증**: 다른 항목(`EstMainDat`, `HeadMainDat` 등)에서 참조 중인 코드가 포함된 경우 숫자 및 문자열 형태의 `LinkIndex`(5, 6, 21, 101, 103, 104, 105)와 연결 코드를 정규화 대조하여 "다른 항목에서 사용 중입니다. 연결을 먼저 해제하세요."로 안전 차단.
|
||||
- 삭제 완료 후 선택 해제 및 블럭 시작/끝 상태 초기화.
|
||||
3) UI 연동 및 700줄 제한 엄수:
|
||||
- `native-block-actions.js`에 `deleteBlockRows` 및 `toggleCostComment`를 모듈화 배치하여 기존 700줄 한도에 도달한 `native-actions.js`(699줄), `native-extra.js`(697줄) 코드 변경 없이 파일 크기 제한 완벽 준수 (`native-block-actions.js`: 185줄).
|
||||
- `index.html` 편집(E) 메뉴에 `블럭 삭제`(`data-action="block-del"`) 및 `산출근거 주석 지정/해제`(`data-action="toggle-cost-comment"`) 등록.
|
||||
3) 원가계산서 공사비역산 자리수 맞추기 (`TFRMWONGABASE popupTotal`):
|
||||
- REA `TFRMWONGABASE` 팝업 `popupTotal` ("공사비역산 자리수 맞추기") 스펙 근거 기반 구현.
|
||||
- `EstWonga.alignTotal(rows, targetCost, unitDigits, options, bases)`:
|
||||
- 원가계산서 총도급공사비(`LinkIndex 55`) 행을 지정 단단위(1,000원, 10,000원 등)로 절하 맞춤.
|
||||
- 단순 Cost 덮어쓰기가 아닌, 실제 `calculate(TotalFix: true)` 엔진을 호출하여 **이윤 및 부가가치세 행을 수렴 조정**하고 전체 원가 명세와 목표 금액(`A+B+C+D === Total`)을 일관되게 맞춤.
|
||||
- **실패 시 안전 롤백 및 기존 상태 보존**: 복제본(`structuredClone`)을 통해 먼저 역산 계산을 수행하며, 순환 참조나 수렴 실패 시 원본 rows의 Cost를 임의 변경하지 않고 기존 상태를 100% 보존하며 에러 처리. 목표 금액 완전 도달 확인 후에만 원본 rows에 결과 동기화.
|
||||
- 조정된 계산 결과를 원본 rows에 정확히 반영하고 `state.project.options.TotalFix = true` 설정.
|
||||
- **저장 및 재열기 왕복 검증**: JSON 직렬화/역직렬화 후 재계산하더라도 이윤, 부가세, 총도급공사비가 변형 없이 동일하게 유지됨을 검증.
|
||||
- UI 연동: 편집(E) 메뉴에 `공사비역산 자리수 맞추기`(`data-action="align-wonga-total"`) 등록 및 `EstActions.form` 연동.
|
||||
4) 레벨 일괄 증감 (`mnuLevelUp`, `mnuLevelDown`, `mnuCostLevelUp`, `mnuCostLevelDown`):
|
||||
- REA `TFRMESTMAIN`/`TFRMEST`의 `mnuLevelUp` ("레벨 일괄증가", `0xd0e374`), `mnuLevelDown` ("레벨 일괄감소") 스펙 근거 기반 구현.
|
||||
- 내역서(`EstMainDat`), 단가산출(`CostMainDat`), 일위대가(`HeadMainDat`), 기계경비(`MachMainDat`)에서 선택된 행들의 `Level`을 일괄 +1 또는 -1 조정(범위: 0~9 클램프).
|
||||
- 지원되지 않는 화면(자재, 노임 등)이나 미선택 시 안전 거부 처리.
|
||||
- UI 연동: 상단 `LEVEL` 메뉴에 `레벨 일괄 증가 (+1)`(`data-action="level-batch-up"`), `레벨 일괄 감소 (-1)`(`data-action="level-batch-down"`) 등록.
|
||||
5) 설계변경 초기화 (`mnuChangeInit`) 및 참조 무결성 보강:
|
||||
- REA `TFRMESTMAIN`의 메뉴 `mnuChangeInit` ("설계변경 초기화") 스펙 근거 기반 구현.
|
||||
- `EstBlockActions.resetChangeAction(state)`:
|
||||
- **불완전 복원 원천 차단**: baseline이 없거나 baseline.tables가 누락된 경우, 무차별적인 `_` 복원을 엄격히 차단하고 "확정된 당초 내역(baseline)이 없습니다. 완전 복원이 불가능하여 초기화를 진행할 수 없습니다." 에러 발생.
|
||||
- **수량 연결 참조 무결성 및 Excel 고유 식별자 동일성 입증 검사**:
|
||||
- 수량산출 연결(`quantities`, `quantitySheets.items`의 `TargetKey`/`TargetCode`): 당초 내역(`baseline.tables.EstMainDat`)에 없는 신규 행을 가리키는 연결 발견 시 즉시 초기화 차단.
|
||||
- Excel 수량 연결(`excelQuantityLinks`): 단순 인덱스 범위 검사가 아니라, 연결 대상의 안정된 고유 식별자(`PkID`)를 통해 당초 행 존재 여부를 대조. 행 중간 삽입·삭제·재정렬이나 중복 코드가 있더라도 `Code`, `Name`, `Size`, `Unite`, `Level`의 동일성(Identity)을 엄격히 검증하여 다른 항목을 가리키는 위험을 원천 차단.
|
||||
- 사용자 확인 대화상자(`confirm`)를 통해 오조작 방지.
|
||||
- **정밀 테이블 및 필드 스펙 격리**: baseline 생성 스펙과 정확히 일치하는 7개 대상 테이블(`EstMainDat`, `HeadMainDat`, `HeadTitleDat`, `CostMainDat`, `CostTitleDat`, `MachTitleDat`, `Wonga`)만 복원하여, 수량 연결(`quantities`), 기성(`progress`), 연차(`annual`) 등 다른 업무 데이터와 연결 구조를 100% 온전하게 보존.
|
||||
- **다른 업무 원본 필드 보존**: 전체 필드의 무차별 `_` 삭제 대신, baseline 스펙에 정의된 18개 핵심 필드(`Code`, `Name`, `Size`, `Unite`, `GN`, `TU`, `TC`, 단위단가 4종, 비목단가 4종, `LineData`, `VSusic`, `Cost`)의 `_` 속성만 선택적으로 정리. 경비성 복원 정보(`_prevSCheck`), 순공사비 복원 정보(`_origLinkIndex`, `_origName`), 주석 복원 정보(`_origIndex`, `_origSCheck`) 등 다른 업무 속성을 완벽하게 보존.
|
||||
- **신규 및 삭제 행 복원**: 설계변경 도중 추가된 신규 행(`NewItem: true`)을 제거하고 삭제된 당초 행을 100% 원래 행 수 및 순서로 원상 복구.
|
||||
- `baseline` 객체를 안전하게 제거하고 프로젝트 작업 모드를 일반 `내역서` 모드로 환원.
|
||||
- **저장 및 재열기 왕복 및 Undo 검증**: 당초 확정 후 변경된 내역을 JSON으로 저장하고 재열기한 상태에서도 `resetChangeAction`을 통해 당초 수량/단가/금액으로 정확히 복원되며, `EstApp.change`를 통한 Undo(실행 취소) 이력 보존 검증 완료.
|
||||
- UI 연동: 편집(E) 메뉴에 `설계변경 초기화`(`data-action="reset-change"`) 등록 및 `handle("reset-change")` 연동.
|
||||
6) 원가계산서 항목 초기화 (`TFRMWONGABASE popupInit`):
|
||||
- REA `TFRMWONGABASE` 팝업 `popupInit` ("항목 초기화") 스펙 근거 기반 구현.
|
||||
- `EstWonga.resetWonga(rows, options, bases)` & `EstBlockActions.resetWongaAction(state)`:
|
||||
- 공사비역산 고정액(`TotalFix: true`) 옵션을 `false`로 해제.
|
||||
- 당초 보존값(`VSusic_`, `Cost_`, `CalcIndex_`)이 있는 경우 원래 수식 및 단가로 안전 복원.
|
||||
- 일반 재계산 엔진(`calculate`)을 호출하여 당초 수식과 직접공사비 기준금액에 따라 원가계산서 전체 항목을 깨끗하게 재계산.
|
||||
- UI 연동: 편집(E) 메뉴에 `원가계산서 초기화`(`data-action="reset-wonga"`) 등록 및 `handle("reset-wonga")` 연동.
|
||||
7) 기성 회차 입력 초기화 (`mnuFileAccomInit`):
|
||||
- REA `TFRMESTMAIN`의 메뉴 `mnuFileAccomInit` ("기성 회차 입력 초기화") 스펙 근거 기반 구현.
|
||||
- `EstBlockActions.resetProgressInputsAction(state)`:
|
||||
- 선택된 기성 회차의 금회 기성 및 사정 입력 데이터(`round.inputs`)를 깨끗하게 리셋.
|
||||
- 확정된 회차(`round.locked: true`)는 입력을 초기화할 수 없도록 안전 차단 ("확정된 회차는 입력을 초기화할 수 없습니다. 확정을 먼저 해제하세요.").
|
||||
- 사용자 확인 대화상자(`confirm`)를 통해 실수로 인한 기성 입력 삭제 방지.
|
||||
- 실행 취소(Undo) 이력 보존 및 JSON 직렬화/역직렬화(저장 재열기) 왕복 보증.
|
||||
- UI 연동: 기성 화면 툴바에 `회차 입력 초기화`(`data-action="reset-round-inputs"`) 버튼 등록 및 `handle("reset-round-inputs")` 연동.
|
||||
8) 연차공사 배분 초기화 (`TFRMANUAL popInit` - 웹 보조 기능 명시):
|
||||
- REA `TFRMANUAL`의 팝업 `popInit` 분석 근거 재확인: REA 원본의 popInit이 전체 차수 삭제인지 단일 차수 수식 리셋인지 동등성 근거가 불충분하므로, 이를 단정하지 않고 웹 보조 기능("연차 차수 배분 초기화 (웹보조)")으로 명확히 구분하여 표기.
|
||||
- `EstBlockActions.resetAnnualAction(state)`:
|
||||
- 등록된 연차공사 차수와 배분 수식(`project.annual = []`)을 웹 보조 차원에서 초기화.
|
||||
- 내역서(`EstMainDat`) 및 원가계산서(`Wonga`)의 본선 원본 데이터를 100% 온전하게 보존.
|
||||
- 빈 상태에서 재호출 시 안전 거부 처리 및 사용자 확인 대화상자(`confirm`)에 웹보조 성격 안내.
|
||||
- 실행 취소(Undo) 지원 및 JSON 저장/재열기 왕복 검증 완료.
|
||||
- UI 연동: 연차공사 화면 상단 툴바에 `연차 차수 초기화(웹보조)`(`data-action="reset-annual"`) 버튼 등록 및 `handle("reset-annual")` 연동.
|
||||
9) 물가변동 입력 초기화 (`TFRMESCALATION popInit`):
|
||||
- REA `TFRMESCALATION`의 팝업 `popInit` ("물가변동 입력 초기화") 스펙 근거 기반 구현.
|
||||
- `EstBlockActions.resetEscalationAction(state)`:
|
||||
- 물가변동 화면(`escalation`)에서 적용제외 수량(`excludeQuantities`) 및 조정 옵션(`options`)을 깨끗하게 리셋.
|
||||
- 내역서 본선 행 및 기준 단가를 100% 온전하게 보존.
|
||||
- 물가변동 미설정 또는 빈 상태에서 재호출 시 안전 거부 처리 및 사용자 확인 대화상자(`confirm`) 지원.
|
||||
- 실행 취소(Undo) 이력 보존 및 JSON 직렬화/역직렬화(저장 재열기) 왕복 보증.
|
||||
- UI 연동: 물가변동 툴바에 `물가변동 입력 초기화`(`data-action="reset-escalation"`) 버튼 연동 및 `handle("reset-escalation")` 등록.
|
||||
9) UI 연동 및 700줄 제한 엄수:
|
||||
- `native-block-actions.js`(636줄), `native-wonga.js`(268줄), `native-extra.js`(699줄)에 모듈화 배치하여 단일 소스코드 파일 700줄 제한 완벽 준수.
|
||||
- `index.html`(225줄) 편집(E) 및 LEVEL 메뉴 등록.
|
||||
- 관련 테스트:
|
||||
- `pytest resources/tester/test_est_row_actions.py` 7개 전수 통과:
|
||||
- `test_toggle_cost_comment_and_block_delete`: 산출근거 행 주석(-1) 지정 및 `_origIndex` 보존, 0으로의 복원 및 임시 속성 제거, 호표 제목 및 구분선 지정 거부, 다중 선택 원자성 보장, 블럭 행 일괄 삭제 및 참조 중인 호표 삭제 차단 검증 완료.
|
||||
- `pytest resources/tester/test_est_row_actions.py` 14개 전수 통과:
|
||||
- `test_reset_escalation_action`: 물가변동 적용제외 수량 및 옵션 초기화, 본선 내역/단가 보존, JSON 저장 재열기 왕복, Undo(실행 취소) 지원, `handle("reset-escalation")` 연동 및 빈 상태 안전 거부 검증 완료.
|
||||
- `test_reset_annual_action`: 연차공사 2개 차수 배분(40%/60%) 계산 검증, JSON 저장 재열기 왕복, `resetAnnualAction` 실행 후 차수 초기화 및 본선 내역/원가 데이터 보존, 빈 상태 안전 거부, `handle("reset-annual")` 연동 및 Undo 지원 검증 완료.
|
||||
- `test_reset_progress_inputs_action`: 미확정 기성 회차의 입력 초기화, 확정된 회차의 초기화 시도 시 안전 차단, JSON 저장 재열기 왕복 후 초기화 검증, `handle("reset-round-inputs")` 연동 및 Undo 지원 검증 완료.
|
||||
- `test_reset_change_action`: baseline 부재 시 안전 차단, 신규 행 참조 수량 연결 시 안전 차단, Excel 수량 연결(`excelQuantityLinks`)의 안정된 식별자(`PkID`) 검증(행 중간 삽입·순서 재정렬·중복 코드 발생 시 신규 행 참조 차단 및 내용 변조 시 동일성 입증 실패 차단), 신규 행 추가 및 기존 행 변경 후 JSON 저장 재열기 왕복, 설계변경 초기화 시 신규 행 제거 및 원래 행/수량/단가 완전 복원, 타 업무 필드(`_origLinkIndex`, `_prevSCheck`) 및 수량 연결(`quantities`) 보존, Undo(실행 취소) 지원 검증 완료.
|
||||
- `test_reset_wonga_action`: 공사비역산 실행(1,250,000원 절하 및 TotalFix=true) 후 JSON 저장 재열기 왕복, 원가계산서 초기화(`resetWongaAction`) 실행 시 TotalFix 해제 및 당초 수식/금액(1,270,500원) 복원, `handle("reset-wonga")` 연동 검증 완료.
|
||||
- `test_align_wonga_total_action`: 원가계산서 10,000원 단단위 절하 맞춤, 실제 `calculate(TotalFix: true)` 호출을 통한 이윤·부가세 역산 조정, 명세 합산 일치(`totalMatch`), JSON 저장 재열기 후 일관성 유지, 순환 참조 등 계산 실패 시 기존 rows 상태 완전 보존 검증 완료.
|
||||
- `test_toggle_cost_comment_and_block_delete`: 산출근거 행 주석(-1) 지정 및 `_origIndex`, `_origSCheck` 보존, 비목 행(-2, -3, -5)의 원래 비목 복원, JSON 저장 재열기 왕복 검증, 호표 제목 및 구분선 지정 거부, 다중 선택 원자성 보장, 블럭 행 일괄 삭제 및 문자열/숫자 `LinkIndex` 참조 호표 삭제 차단 검증 완료.
|
||||
- `test_level_batch_adjust_actions`: 선택 행 레벨 일괄 +1/-1 증감, 0~9 상하한 클램프, 비지원 테이블 및 미선택 시 안전 거부 검증 완료.
|
||||
- `test_block_actions_range_selection_and_clear`: 블럭 시작/끝 지정, 범위 선택, 전체 선택, 블럭 해제 및 예외 검증 완료.
|
||||
- `pytest resources/tester/test_est_row_actions.py resources/tester/test_est_estimate.py resources/tester/test_est_business.py`: 29 passed in 21.24s.
|
||||
- `pytest resources/tester/test_est_row_actions.py resources/tester/test_est_estimate.py resources/tester/test_est_business.py`: 36 passed in 21.60s.
|
||||
|
||||
- [x] 단가산출 산출근거 비목 지정/해제(`popMainSetNC/GC/JC/TC/BimogFree`) 구현 및 순공사비·경비성 안전 가드 보강
|
||||
- 자체 검증:
|
||||
@@ -353,6 +422,14 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD
|
||||
- 대용량 임시 저장 보완: `native-app.js`에서 sessionStorage(약 5~10MB) 용량 초과 시 IndexedDB(`est-independent-cache`)로 자동 대체 저장·복원하도록 개선하여, 대용량(main.estx 약 157MB JSON) 작업 시 새로고침 유실 문제를 해결함. IndexedDB 트랜잭션 `waitForTx(tx)` 대기 및 순차 직렬화 큐(`cacheQueue`)를 적용해 연속 입력 시 덮어쓰기 레이스 컨디션을 방지하고, 탭별 고유 인스턴스 ID(`sessionStorage`의 `est-tab-instance-id`) 및 Chromium 탭 복제 시 `window.name` 리셋 감지를 적용해 복제 탭(Duplicate tab) 분리 격리 및 부모 작업본 초기 상속을 구현함. 실제 headless Edge 기반 Playwright 회귀(`resources/tester/test_est_browser_cache.py`)로 소형 데이터 격리, 대용량 `main.estx`(22MB 바이너리, 157MB JSON) 업로드 후 이름 수정→IndexedDB 저장→새로고침 복원(76개 전체 내역행 유지), 캐시 실패 안내 메시지 검증 및 탭 복제(Duplicate Tab) 상속·독립 격리 시험 통과(3 passed in 13.04s).
|
||||
- 백그라운드 상태 감시 및 자동 알림: `tools/est-test/monitor.cjs`를 통해 15초 주기로 `orca.exe` CLI(`shell: false`, timeout 4~5s)를 호출하여 CODEX 워크트리의 안티그래비티 작업 상태와 EST 5175 서버 헬스를 조회하고, 상태 변경 시에만 `monitor.log`에 기록(최대 512KB 순환). 안티그래비티 완료/대기 전이 시 Codex Orchestration Run(`run_cdb1558b9280`)으로 단발성 자동 알림(`status`/`messageId=msg_2c7bca748f91`)이 실제 전달됨을 확인(중복 폭주 방지 플래그 적용, 민감 환경변수 미기록, 재시작/자동지시 없음).
|
||||
## Codex · EST 독립 테스트 앱 (2026-11-04)
|
||||
|
||||
- [x] 기성 회차 초기화가 선택한 회차만 변경하도록 보호하고 오류 시 입력 보존을 확인한다.
|
||||
- 자체 검증: 존재하지 않는 회차를 선택했을 때 첫 회차를 초기화하지 않고 오류를 표시하며 기존 입력이 보존되는 회귀 시험 통과 (`test_reset_progress_inputs_action`, 1 passed).
|
||||
- [x] 설계변경 초기화 시 수량 연결 대상을 실제 수량 반영 규칙과 동일하게 검사한다.
|
||||
- 자체 검증: 유효하지 않은 TargetKey가 같은 코드로 잘못 대체되는 경우와 중복 코드 연결을 차단한다 (`test_reset_change_action`, 통과).
|
||||
- [x] 확정된 전회 기성 결과가 없을 때 금회 누계를 잘못 확정하지 않도록 보호한다.
|
||||
- 자체 검증: 전회 결과가 없는 확정 회차 다음 계산에 오류를 표시하고 확정을 거부한다. 기성·행 조작 시험 24개 통과.
|
||||
- 추가 회귀: 설계변경·기성·물가변동·연차 관련 시험 79개 통과(기성·수량·행 조작 35개, 업무·설계변경·물가변동·연차 44개).
|
||||
- [x] 일위대가 상세 비목 지정 변경을 해당 행에만 반영 → 자동·수동 계산, 원천 불일치·다른 호표 보존 시험.
|
||||
- 자체 검증: 관련 시험 33개 통과. Tailscale HTTP Edge에서 자동·수동 계산, 다른 호표 보존, 원천 단가 불일치 시 복원 차단 및 화면 오류 0 확인.
|
||||
- [x] 원천 재조회가 꺼진 내역의 비목 지정 변경을 해당 행에만 반영 → 자동·수동 계산, 미연결·다른 행 보존 시험.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// REA popMainBlockCut, popMainBlockCopy, popMainBlockPaste, popMainBlockDel 및 블럭 범위 지정 (ActionBlockOneSet, 블럭시작/끝)
|
||||
// 및 popMainSetStr (산출근거 주석 지정/해제), TFRMWONGABASE popupTotal (공사비역산 자리수 맞추기)
|
||||
// 및 mnuLevelUp, mnuLevelDown, mnuCostLevelUp, mnuCostLevelDown (레벨 일괄 증가/감소)
|
||||
const EstBlockActions = (() => {
|
||||
let blockStart = null;
|
||||
let blockTable = null;
|
||||
@@ -224,13 +225,331 @@ const EstBlockActions = (() => {
|
||||
],
|
||||
({ targetCost, unitDigits }) => {
|
||||
EstApp.change(() => {
|
||||
EstWonga.alignTotal(rows, targetCost, unitDigits);
|
||||
const direct = state.project.calculation?.direct || {};
|
||||
const bases = {
|
||||
NC: Number(direct.NC) || 0,
|
||||
GC: Number(direct.GC) || 0,
|
||||
JC: Number(direct.JC) || 0,
|
||||
SC: Number(direct.SC) || 0,
|
||||
TC: Number(direct.TC) || 0,
|
||||
노무비: Number(direct.NC) || 0,
|
||||
재료비: Number(direct.GC) || 0,
|
||||
경비: Number(direct.JC) || 0,
|
||||
실적: Number(direct.SC) || 0,
|
||||
직접공사비: Number(direct.TC) || 0,
|
||||
};
|
||||
EstWonga.alignTotal(rows, targetCost, unitDigits, state.project.options, bases);
|
||||
state.project.options.TotalFix = true;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function resetChangeAction(state) {
|
||||
// REA mnuChangeInit: 설계변경 초기화 (확정된 당초 내역으로 완전 복원 및 변경 사본 제거)
|
||||
const project = state.project;
|
||||
// 당초 확정 정보(baseline)가 없는 불완전 상태에서의 복원 시도는 데이터 유실 위험이 있으므로 엄격 차단
|
||||
if (!project.baseline || !project.baseline.tables) {
|
||||
throw Error(
|
||||
"확정된 당초 내역(baseline)이 없습니다. 완전 복원이 불가능하여 초기화를 진행할 수 없습니다.",
|
||||
);
|
||||
}
|
||||
if (project.options.Mode !== "설계변경" && project.baseline == null) {
|
||||
throw Error("설계변경 모드가 아닙니다.");
|
||||
}
|
||||
|
||||
// 1) 수량 연결(quantities, quantitySheets, excelQuantityLinks) 참조 무결성 사전 검사
|
||||
const baselineEstMain = project.baseline.tables.EstMainDat || [];
|
||||
|
||||
// 수량산출서 연결 검사
|
||||
const qtyItems = [];
|
||||
if (Array.isArray(project.quantities)) {
|
||||
qtyItems.push(...project.quantities);
|
||||
}
|
||||
if (Array.isArray(project.quantitySheets)) {
|
||||
for (const s of project.quantitySheets) {
|
||||
if (Array.isArray(s.items)) qtyItems.push(...s.items);
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of qtyItems) {
|
||||
const code = item.TargetCode ?? item.LinkCode;
|
||||
const key = item.TargetKey;
|
||||
if (!code && !key) continue;
|
||||
|
||||
const targets = baselineEstMain.filter(
|
||||
(row) =>
|
||||
!Number(row.Level) &&
|
||||
(key ? row.PkID != null && `id:${row.PkID}` === key : row.Code === code),
|
||||
);
|
||||
if (targets.length !== 1) {
|
||||
throw Error(
|
||||
`수량산출 연결 무결성 오류: 신규 행을 참조하는 수량 연결(${key || code})이 존재하여 당초 내역 복원 시 연결이 끊어집니다. 해당 수량 연결을 먼저 해제하거나 변경하세요.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Excel 수량 연결 검사 (안정된 고유 식별자 PkID 및 항목 내용 동일성 엄격 검증)
|
||||
if (Array.isArray(project.excelQuantityLinks) && project.excelQuantityLinks.length > 0) {
|
||||
const currentEstMain = project.tables.EstMainDat || [];
|
||||
const currentRowsByPkID = new Map();
|
||||
currentEstMain.forEach((r, idx) => {
|
||||
if (r.PkID != null) currentRowsByPkID.set(Number(r.PkID), { row: r, index: idx });
|
||||
});
|
||||
|
||||
const baselineRowsByPkID = new Map();
|
||||
baselineEstMain.forEach((r, idx) => {
|
||||
if (r.PkID != null) baselineRowsByPkID.set(Number(r.PkID), { row: r, index: idx });
|
||||
});
|
||||
|
||||
for (const link of project.excelQuantityLinks) {
|
||||
if (!link || link.target == null) continue;
|
||||
const targetId = Number(link.target);
|
||||
|
||||
// 1) 현재 작업본에 대상 행이 존재하는지 확인
|
||||
const curInfo = currentRowsByPkID.get(targetId);
|
||||
if (!curInfo) {
|
||||
throw Error(
|
||||
`Excel 수량 연결 무결성 오류: 연결 대상 행(PkID ${targetId})이 현재 내역에 없습니다. 해당 연결을 먼저 정리하세요.`,
|
||||
);
|
||||
}
|
||||
|
||||
// 2) 당초 확정본(baseline)에 대상 행이 존재하는지 확인 (신규 행 참조 차단)
|
||||
const baseInfo = baselineRowsByPkID.get(targetId);
|
||||
if (!baseInfo) {
|
||||
throw Error(
|
||||
`Excel 수량 연결 무결성 오류: 신규 내역 행(PkID ${targetId}, ${curInfo.row.Code || curInfo.row.Name || ""})을 참조하는 Excel 연결(${link.file} ${link.cell})이 존재하여 당초 내역 복원 시 연결이 끊어집니다. 해당 연결을 먼저 해제하세요.`,
|
||||
);
|
||||
}
|
||||
|
||||
// 3) 동일성(Identity) 입증: Code, Name, Size, Unite 및 상세행 조건이 당초 행과 일치하는지 대조
|
||||
const curRow = curInfo.row;
|
||||
const baseRow = baseInfo.row;
|
||||
const isSameIdentity =
|
||||
String(curRow.Code || "") === String(baseRow.Code || "") &&
|
||||
String(curRow.Name || "") === String(baseRow.Name || "") &&
|
||||
String(curRow.Size || "") === String(baseRow.Size || "") &&
|
||||
String(curRow.Unite || "") === String(baseRow.Unite || "") &&
|
||||
Number(curRow.Level || 0) === Number(baseRow.Level || 0);
|
||||
|
||||
if (!isSameIdentity) {
|
||||
throw Error(
|
||||
`Excel 수량 연결 무결성 오류: 연결 대상 행(PkID ${targetId})의 내용(${curRow.Name})이 당초 행(${baseRow.Name})과 일치하지 않아 다른 항목을 가리키는 위험이 있습니다. 동일성이 입증되지 않아 초기화를 차단합니다.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
!confirm(
|
||||
"설계변경 작업을 초기화하고 당초 내역 상태로 되돌리시겠습니까?\n(입력된 변경 수량 및 단가가 모두 당초 상태로 환원됩니다)",
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 설계변경 대상 7개 테이블 목록 (EstBusiness.baseline 스펙 일치)
|
||||
const changeTables = [
|
||||
"EstMainDat",
|
||||
"HeadMainDat",
|
||||
"HeadTitleDat",
|
||||
"CostMainDat",
|
||||
"CostTitleDat",
|
||||
"MachTitleDat",
|
||||
"Wonga",
|
||||
];
|
||||
|
||||
// 설계변경 관련 특정 필드 목록 (EstBusiness.baseline 스펙 일치)
|
||||
const baselineFields = [
|
||||
"Code",
|
||||
"Name",
|
||||
"Size",
|
||||
"Unite",
|
||||
"GN",
|
||||
"TU",
|
||||
"TC",
|
||||
"NU",
|
||||
"GU",
|
||||
"JU",
|
||||
"SU",
|
||||
"NC",
|
||||
"GC",
|
||||
"JC",
|
||||
"SC",
|
||||
"LineData",
|
||||
"VSusic",
|
||||
"Cost",
|
||||
];
|
||||
|
||||
EstApp.change(() => {
|
||||
// 1) baseline.tables에 존재하는 7개 대상 테이블만 당초 상태로 완전 복원 (신규/삭제 행 및 수량/단가 완벽 복원)
|
||||
for (const tName of changeTables) {
|
||||
if (project.baseline.tables[tName]) {
|
||||
project.tables[tName] = structuredClone(project.baseline.tables[tName]);
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 대상 테이블의 행들에서만 설계변경 관련 _ 접미사 필드 제거 (다른 업무 원본 필드, _origLinkIndex, _prevSCheck 등 완벽 보존)
|
||||
for (const tName of changeTables) {
|
||||
const rows = project.tables[tName];
|
||||
if (!Array.isArray(rows)) continue;
|
||||
for (const row of rows) {
|
||||
row.NewItem = false;
|
||||
for (const f of baselineFields) {
|
||||
delete row[`${f}_`];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) baseline 제거 및 모드 환원
|
||||
delete project.baseline;
|
||||
project.options.Mode = "내역서";
|
||||
});
|
||||
state.notice = "설계변경이 초기화되어 당초 내역 상태로 복원되었습니다.";
|
||||
}
|
||||
|
||||
function resetWongaAction(state) {
|
||||
// REA TFRMWONGABASE popupInit: 원가계산서 항목 초기화
|
||||
const rows = state.project.tables.Wonga;
|
||||
if (!rows || !rows.length) {
|
||||
throw Error("원가계산서 자료가 없습니다.");
|
||||
}
|
||||
|
||||
if (
|
||||
!confirm(
|
||||
"원가계산서 항목을 초기화하시겠습니까?\n(공사비역산 고정액이 해제되고 당초 수식 및 기준 단가로 재계산됩니다)",
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
EstApp.change(() => {
|
||||
const direct = state.project.calculation?.direct || {};
|
||||
const bases = {
|
||||
NC: Number(direct.NC) || 0,
|
||||
GC: Number(direct.GC) || 0,
|
||||
JC: Number(direct.JC) || 0,
|
||||
SC: Number(direct.SC) || 0,
|
||||
TC: Number(direct.TC) || 0,
|
||||
노무비: Number(direct.NC) || 0,
|
||||
재료비: Number(direct.GC) || 0,
|
||||
경비: Number(direct.JC) || 0,
|
||||
실적: Number(direct.SC) || 0,
|
||||
직접공사비: Number(direct.TC) || 0,
|
||||
};
|
||||
state.project.options.TotalFix = false;
|
||||
EstWonga.resetWonga(rows, state.project.options, bases);
|
||||
});
|
||||
state.notice = "원가계산서 항목이 초기화되었습니다.";
|
||||
}
|
||||
function resetProgressInputsAction(state) {
|
||||
// REA mnuFileAccomInit: 기성 회차 입력 초기화 (선택된 회차의 금회/사정 입력 및 모드를 초기화)
|
||||
const project = state.project;
|
||||
if (!project.progress || !project.progress.length) {
|
||||
throw Error("기성 회차 자료가 없습니다.");
|
||||
}
|
||||
const round = project.progress.find((r) => r.id === state.roundId);
|
||||
if (!round) {
|
||||
throw Error("선택된 기성 회차가 없습니다.");
|
||||
}
|
||||
if (round.locked) {
|
||||
throw Error("확정된 회차는 입력을 초기화할 수 없습니다. 확정을 먼저 해제하세요.");
|
||||
}
|
||||
|
||||
if (
|
||||
!confirm(
|
||||
`[${round.name || "현재 회차"}]의 모든 금회 기성 및 사정 입력을 초기화하시겠습니까?\n(입력된 기성 수량과 비율이 모두 지워집니다)`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
EstApp.change(() => {
|
||||
round.inputs = {};
|
||||
delete round.result;
|
||||
}, false);
|
||||
state.notice = `[${round.name || "현재 회차"}]의 기성 입력이 초기화되었습니다.`;
|
||||
}
|
||||
|
||||
function resetAnnualAction(state) {
|
||||
// REA 연차공사: 등록된 차수 배분 설정 초기화 (웹 전용 보조 기능, 원본 동등성 미확인)
|
||||
// 주의: REA 원본의 popInit은 단일 차수 수식/단가 초기화일 가능성이 있어 전체 차수 삭제와의 동등성은 미확인입니다.
|
||||
// 본 기능은 등록된 연차 차수 목록을 비우는 웹 전용 보조 기능으로 명확히 구분하여 제공합니다.
|
||||
const project = state.project;
|
||||
if (!project.annual || !project.annual.length) {
|
||||
throw Error("연차공사 차수 자료가 없습니다.");
|
||||
}
|
||||
|
||||
if (
|
||||
!confirm(
|
||||
"등록된 모든 연차공사 차수와 배분 수식을 초기화하시겠습니까?\n(웹 보조 기능: 기존 내역서 및 원가계산서 데이터는 유지됩니다)",
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
EstApp.change(() => {
|
||||
project.annual = [];
|
||||
}, false);
|
||||
state.notice = "연차공사 차수 및 배분 설정이 초기화되었습니다 (웹 보조 기능).";
|
||||
}
|
||||
function resetEscalationAction(state) {
|
||||
// REA 물가변동: 적용제외 수량 및 입력 초기화 (TFRMESCALATION popInit 스펙 기반)
|
||||
const rows = state.project.tables.EstMainDat;
|
||||
if (!rows || !rows.length) {
|
||||
throw Error("내역서 자료가 없습니다.");
|
||||
}
|
||||
|
||||
if (
|
||||
!confirm(
|
||||
"물가변동 적용제외 수량과 입력값을 초기화하시겠습니까?\n(예정·실행·기성·수동제외 수량이 초기화되고 적용제외 자동계산이 기본값으로 환원됩니다)",
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
EstApp.change(() => {
|
||||
for (const row of rows) {
|
||||
delete row.EscPlanned;
|
||||
delete row.EscActual;
|
||||
delete row.EscProgress;
|
||||
delete row.EscManualQuantity;
|
||||
delete row.EscConstruction;
|
||||
delete row.EscPS;
|
||||
delete row.EscBefore;
|
||||
delete row.EscAuto;
|
||||
}
|
||||
delete state.project.options.EscDesignTotal;
|
||||
}, false);
|
||||
state.notice = "물가변동 적용제외 수량 및 설정이 초기화되었습니다.";
|
||||
}
|
||||
|
||||
function adjustLevels(state, delta) {
|
||||
// REA mnuLevelUp, mnuLevelDown, mnuCostLevelUp, mnuCostLevelDown: 선택 행 레벨 일괄 증가/감소
|
||||
const rows = state.project.tables[state.view];
|
||||
if (
|
||||
!rows ||
|
||||
!["EstMainDat", "CostMainDat", "HeadMainDat", "MachMainDat"].includes(state.view)
|
||||
) {
|
||||
throw Error("내역서, 단가산출, 일위대가, 기계경비 화면에서만 레벨을 조정할 수 있습니다.");
|
||||
}
|
||||
const selected = [...state.selection].sort((a, b) => a - b);
|
||||
if (!selected.length) {
|
||||
throw Error("레벨을 조정할 행을 선택하세요.");
|
||||
}
|
||||
|
||||
EstApp.change(() => {
|
||||
for (const idx of selected) {
|
||||
const r = rows[idx];
|
||||
if (!r) continue;
|
||||
const curLevel = Number(r.Level) || 0;
|
||||
// Level 범위: 0 ~ 9
|
||||
const newLevel = Math.max(0, Math.min(9, curLevel + delta));
|
||||
r.Level = newLevel;
|
||||
}
|
||||
});
|
||||
state.notice = `${selected.length}개 행의 레벨이 일괄 ${delta > 0 ? "증가" : "감소"}되었습니다.`;
|
||||
}
|
||||
|
||||
function handle(name, state) {
|
||||
if (name === "block-start") {
|
||||
setBlockStart(state);
|
||||
@@ -260,6 +579,34 @@ const EstBlockActions = (() => {
|
||||
alignWongaTotalAction(state);
|
||||
return true;
|
||||
}
|
||||
if (name === "reset-escalation") {
|
||||
resetEscalationAction(state);
|
||||
return true;
|
||||
}
|
||||
if (name === "reset-annual") {
|
||||
resetAnnualAction(state);
|
||||
return true;
|
||||
}
|
||||
if (name === "reset-round-inputs") {
|
||||
resetProgressInputsAction(state);
|
||||
return true;
|
||||
}
|
||||
if (name === "reset-wonga") {
|
||||
resetWongaAction(state);
|
||||
return true;
|
||||
}
|
||||
if (name === "level-batch-up") {
|
||||
adjustLevels(state, 1);
|
||||
return true;
|
||||
}
|
||||
if (name === "reset-change") {
|
||||
resetChangeAction(state);
|
||||
return true;
|
||||
}
|
||||
if (name === "level-batch-down") {
|
||||
adjustLevels(state, -1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -271,6 +618,12 @@ const EstBlockActions = (() => {
|
||||
deleteBlockRows,
|
||||
toggleCostComment,
|
||||
alignWongaTotalAction,
|
||||
resetEscalationAction,
|
||||
resetAnnualAction,
|
||||
resetProgressInputsAction,
|
||||
resetWongaAction,
|
||||
resetChangeAction,
|
||||
adjustLevels,
|
||||
handle,
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -139,6 +139,7 @@ const EstExtra = (() => {
|
||||
button("fill-progress", "전량·비율 일괄 입력") +
|
||||
button("rename-round", "회차 이름") +
|
||||
button("delete-round", "회차 삭제") +
|
||||
button("reset-round-inputs", "회차 입력 초기화") +
|
||||
`<label><input id="progress-components" type="checkbox" ${round.separateComponents !== false ? "checked" : ""} ${round.locked ? "disabled" : ""}> 비목별 계산</label><label>금액 소수자리 <input id="progress-digits" type="number" min="0" max="4" value="${round.amountDigits ?? 0}" ${round.locked ? "disabled" : ""}></label>` +
|
||||
`<label><input id="use-approved" type="checkbox" ${round.approved ? "checked" : ""} ${round.locked ? "disabled" : ""}> 누계에 사정 사용</label>`;
|
||||
let output;
|
||||
@@ -284,6 +285,7 @@ const EstExtra = (() => {
|
||||
button("load-bid", "입찰단가 불러오기") +
|
||||
button("load-contract", "계약 내역 불러오기") +
|
||||
button("load-progress-exclusion", "기성 누계수량 불러오기") +
|
||||
button("reset-escalation", "물가변동 입력 초기화") +
|
||||
button("capture-baseline", "계약 내역 확정") +
|
||||
`<label>조사 도급액(0=단가로 계산) <input type="number" step="any" data-option="EscDesignTotal" value="${project.options.EscDesignTotal ?? 0}"></label>`,
|
||||
table(
|
||||
@@ -316,7 +318,7 @@ const EstExtra = (() => {
|
||||
state.business = output;
|
||||
html = box(
|
||||
"연차공사",
|
||||
button("add-annual", "차수 추가"),
|
||||
button("add-annual", "차수 추가") + button("reset-annual", "연차 차수 초기화(웹보조)"),
|
||||
table(
|
||||
["차수", "수량·금액 배분 수식", "순공사비", "도급액", "작업", ""],
|
||||
project.annual.map((year, index) => [
|
||||
|
||||
@@ -44,6 +44,8 @@ const EstProgress = (() => {
|
||||
if (earlier.some((item) => !item.locked)) errors.push("전회 기성 회차를 먼저 확정하세요.");
|
||||
const previous = earlier.at(-1)?.result,
|
||||
oldRows = new Map((previous?.rows || []).map((row, index) => [key(row, index), row]));
|
||||
if (earlier.at(-1)?.locked && !Array.isArray(previous?.rows))
|
||||
errors.push("확정된 전회 기성 결과가 없습니다. 전회 결과를 확인하세요.");
|
||||
const segments = { Gum: [], Sa: [] };
|
||||
const separated = round.separateComponents !== false;
|
||||
rows.forEach((row, index) => {
|
||||
|
||||
@@ -108,8 +108,7 @@ const EstWonga = (() => {
|
||||
|
||||
function calculate(input, options = {}, bases = {}) {
|
||||
const rows = structuredClone(input || []),
|
||||
errors = [],
|
||||
adjustments = [];
|
||||
errors = [];
|
||||
const overrides = new Map();
|
||||
|
||||
function pass() {
|
||||
@@ -128,12 +127,6 @@ const EstWonga = (() => {
|
||||
if (profit < 0) throw Error("도급액 고정: 조정할 이윤 행이 없습니다.");
|
||||
for (let iteration = 0; iteration < 100; iteration++) {
|
||||
const delta = num(rows[targetIndex].Cost) - target;
|
||||
adjustments.push({
|
||||
iteration,
|
||||
profit: num(rows[profit].Cost),
|
||||
total: num(rows[targetIndex].Cost),
|
||||
difference: delta,
|
||||
});
|
||||
if (delta === 0) break;
|
||||
overrides.set(profit, num(rows[profit].Cost) - delta);
|
||||
pass();
|
||||
@@ -151,7 +144,6 @@ const EstWonga = (() => {
|
||||
return {
|
||||
rows,
|
||||
errors,
|
||||
adjustments,
|
||||
total: targetIndex >= 0 ? num(rows[targetIndex].Cost) : num(rows.at(-1)?.Cost),
|
||||
};
|
||||
}
|
||||
@@ -176,9 +168,9 @@ const EstWonga = (() => {
|
||||
return errors;
|
||||
}
|
||||
|
||||
function alignTotal(rows, targetCost, unitDigits = 1) {
|
||||
function alignTotal(rows, targetCost, unitDigits = 1, options = {}, bases = {}) {
|
||||
// REA TFRMWONGABASE popupTotal: 공사비역산 자리수 맞추기
|
||||
// unitDigits: 1=천원단위(1000), 2=만원단위(10000), 0=원단위(1) 등
|
||||
// unitDigits: 1=십원(10), 2=백원(100), 3=천원(1000), 4=만원(10000), 0=원단위(1)
|
||||
const target = Number(targetCost);
|
||||
if (!Number.isFinite(target) || target <= 0) {
|
||||
throw Error("목표 공사금액을 0보다 큰 숫자로 입력하세요.");
|
||||
@@ -191,7 +183,7 @@ const EstWonga = (() => {
|
||||
throw Error("원가계산서에 총도급공사비(LinkIndex 55) 행이 없습니다.");
|
||||
}
|
||||
|
||||
// 조정 대상 행: 이윤 행 또는 일반 경비 행 탐색
|
||||
// 조정 대상 행: 이윤 행
|
||||
const profitIndex = rows.findIndex(
|
||||
(r) => compact(r.Name).includes("이윤") && ![1, 3].includes(num(r.CalcIndex)),
|
||||
);
|
||||
@@ -199,19 +191,78 @@ const EstWonga = (() => {
|
||||
throw Error("단단위 조정을 반영할 이윤 행을 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
// 도급액을 divisor 단위로 절하(또는 절상/반올림 정렬)한 목표값 설정
|
||||
// 도급액을 divisor 단위로 절하한 목표값 설정
|
||||
const roundedTarget = Math.floor(target / divisor) * divisor;
|
||||
rows[targetRowIndex].Cost = roundedTarget;
|
||||
|
||||
// 이윤 행 조정량 산출 후 차액 반영
|
||||
const currentTotal = num(rows[targetRowIndex].Cost);
|
||||
// 실패 시 기존 rows 상태를 100% 보존하기 위해 복제본으로 먼저 계산
|
||||
const testRows = structuredClone(rows);
|
||||
testRows[targetRowIndex].Cost = roundedTarget;
|
||||
|
||||
// 실제 calculate(TotalFix: true) 수행하여 이윤·부가세·전체 원가 명세와 목표 금액을 일치시킴
|
||||
const calcResult = calculate(testRows, { ...options, TotalFix: true }, bases);
|
||||
if (calcResult.errors && calcResult.errors.length) {
|
||||
throw Error(`원가역산 계산 오류: ${calcResult.errors.join("; ")}`);
|
||||
}
|
||||
|
||||
// 수렴 실패 또는 목표 불일치 시 기존 rows 변경 없이 에러 차단
|
||||
if (num(calcResult.rows[targetRowIndex]?.Cost) !== roundedTarget) {
|
||||
throw Error("원가역산 도급액 고정 계산이 목표 금액에 수렴하지 못했습니다.");
|
||||
}
|
||||
|
||||
// 계산이 성공하고 목표에 완전히 도달한 경우에만 일관성 있는 Cost 결과를 원본 rows에 반영
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
if (calcResult.rows[i]) {
|
||||
rows[i].Cost = calcResult.rows[i].Cost;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
targetIndex: targetRowIndex,
|
||||
profitIndex,
|
||||
roundedTarget,
|
||||
divisor,
|
||||
finalTotal: num(rows[targetRowIndex].Cost),
|
||||
finalProfit: num(rows[profitIndex].Cost),
|
||||
};
|
||||
}
|
||||
|
||||
return { calculate, scan, alignTotal };
|
||||
function resetWonga(rows, options = {}, bases = {}) {
|
||||
// REA TFRMWONGABASE popupInit: 원가계산서 항목 초기화 (TotalFix 해제, 수렴 override 초기화, 당초 수식 및 Cost 복원 후 재계산)
|
||||
if (!Array.isArray(rows) || !rows.length) {
|
||||
throw Error("원가계산서 행 자료가 없습니다.");
|
||||
}
|
||||
|
||||
// 1) 당초 보존값(VSusic_, Cost_, CalcIndex_)이 있는 경우 복원
|
||||
for (const r of rows) {
|
||||
if (r.VSusic_ !== undefined) {
|
||||
r.VSusic = r.VSusic_;
|
||||
}
|
||||
if (r.Cost_ !== undefined) {
|
||||
r.Cost = num(r.Cost_);
|
||||
}
|
||||
if (r.CalcIndex_ !== undefined) {
|
||||
r.CalcIndex = r.CalcIndex_;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) TotalFix 옵션 해제 및 일반 재계산 수행
|
||||
const newOptions = { ...options, TotalFix: false };
|
||||
const calcResult = calculate(rows, newOptions, bases);
|
||||
if (calcResult.errors && calcResult.errors.length) {
|
||||
throw Error(`원가계산서 초기화 재계산 오류: ${calcResult.errors.join("; ")}`);
|
||||
}
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
if (calcResult.rows[i]) {
|
||||
rows[i].Cost = calcResult.rows[i].Cost;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
rows,
|
||||
total: calcResult.total,
|
||||
};
|
||||
}
|
||||
|
||||
return { calculate, scan, alignTotal, resetWonga };
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user