diff --git a/resources/tester/test_est_quantity_workflow.py b/resources/tester/test_est_quantity_workflow.py index 7429b3b17..f09de4c74 100644 --- a/resources/tester/test_est_quantity_workflow.py +++ b/resources/tester/test_est_quantity_workflow.py @@ -136,3 +136,27 @@ def test_new_quantity_code_does_not_reuse_existing_or_referenced_codes(): EstQuantity.newCode([{Code:'Q2',formula:'1'}]), EstQuantity.newCode([])]""", ) assert result == ["Q3", "Q1", "Q1"] + + +def test_target_row_ids_support_blank_and_duplicate_codes_for_both_modes(): + result = run( + "math", + """(()=>{ + const p=EstNative.empty();p.tables.EstMainDat=[ + {PkID:1,Code:'X',GN:1},{PkID:2,Code:'X',GN:2},{PkID:3,Code:'',GN:3}]; + p.quantities=[{Code:'A',formula:'7',TargetCode:'X',TargetKey:'id:2'}, + {Code:'B',formula:'4',TargetCode:'',TargetKey:'id:3'}]; + EstQuantity.applyTotals(p,EstQuantity.totals(p,'manual')); + const manual=p.tables.EstMainDat.map(row=>row.GN); + p.quantitySheets=[{Code:'Q',options:{},rows:[{LineData:'2+3 = \\x120\\x12',markers:['r1']}], + items:[{Code:'r1',GN:0,TargetCode:'X',TargetKey:'id:1'}]}]; + p.tables.EstMainDat.reverse();EstQuantity.applyTotals(p,EstQuantity.totals(p,'Q')); + const sheet=p.tables.EstMainDat.map(row=>[row.PkID,row.GN]); + p.quantitySheets[0].items[0].TargetKey='id:missing';let error=''; + try{EstQuantity.totals(p,'Q')}catch(e){error=e.message} + return {manual,sheet,error}; + })()""", + ) + assert result["manual"] == [1, 7, 4] + assert result["sheet"] == [[3, 4], [2, 7], [1, 5]] + assert result["error"] diff --git a/tools/est-test/README.md b/tools/est-test/README.md index 9331dd4c9..cc1dedc32 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -25,7 +25,7 @@ Ctrl+S는 입력 중인 셀도 확정한 뒤 저장합니다. 연속 저장 요 초기 자료는 기술정보 DB의 실제 REA 저장 샘플입니다. `REA 샘플 대조`에서 원본 저장값과 275개 계산 결과를 비교합니다. 기존 ESTX의 기본 수량 연결은 문서·항목 코드와 내역 코드가 유일한 경우 화면에 표시하며, 중복·다른 모드는 원문에 보존하고 경고합니다. 원본 연결 데이터 실자료 검증과 새 연결의 ESTX 쓰기는 아직 지원하지 않습니다. 테스트용 임의 요율을 원본 기본값으로 넣지 않습니다. 새 원가 서식의 요율은 사용자가 입력합니다. -수량산출서에서 내역에 수량을 반영할 때 연결 코드에 해당하는 상세 내역이 정확히 하나여야 합니다. 코드가 없거나 중복되면 다른 항목을 포함해 반영을 중단하고 오류를 표시합니다. +수량산출서와 수식표에서 내역 행을 직접 선택해 연결할 수 있습니다. 내역 행 선택을 사용하면 코드가 없거나 중복되어도 해당 행에만 수량을 반영합니다. 코드를 직접 입력할 때는 해당 상세 내역이 정확히 하나여야 하며, 대상이 없거나 중복되면 다른 항목을 포함해 반영을 중단하고 오류를 표시합니다. 수량 화면에서 새 산출서를 만들고 이름과 산출 항목의 품명·규격을 편집할 수 있습니다. `산출 행 추가`로 결과 표지가 연결된 행과 항목을 함께 만든 뒤 수식을 입력하고, 연결 내역코드를 지정해 계산 수량을 내역에 반영합니다. 행 아래 삽입, 위·아래 이동과 산출 행·산출서 삭제도 가능하며, 이 구조 변경은 JSON 저장과 다시 열기를 지원합니다. 기존 연결은 내역코드를 비워 해제할 수 있습니다. diff --git a/tools/est-test/native-actions.js b/tools/est-test/native-actions.js index 57c17ec32..bf9b2c1d6 100644 --- a/tools/est-test/native-actions.js +++ b/tools/est-test/native-actions.js @@ -545,11 +545,12 @@ const EstActions = (() => { if (node.matches("[data-extra]")) { const value = node.type === "number" ? Number(node.value) : node.value; if (node.type === "number" && !Number.isFinite(value)) throw Error("수치 입력 오류"); - EstApp.change( - () => - (project[node.dataset.extra][Number(node.dataset.row)][node.dataset.field] = value), - false, - ); + EstApp.change(() => { + const row = project[node.dataset.extra][Number(node.dataset.row)]; + row[node.dataset.field] = value; + if (node.dataset.extra === "quantities" && node.dataset.field === "TargetCode") + row.TargetKey = ""; + }, false); return; } if (node.matches("[data-table][data-field]")) { diff --git a/tools/est-test/native-extra.js b/tools/est-test/native-extra.js index 0a53fa34d..eadc22255 100644 --- a/tools/est-test/native-extra.js +++ b/tools/est-test/native-extra.js @@ -28,7 +28,8 @@ const EstExtra = (() => { return [ field("quantities", index, "Code", row.Code, "text"), field("quantities", index, "Name", row.Name, "text"), - field("quantities", index, "TargetCode", row.TargetCode, "text"), + field("quantities", index, "TargetCode", row.TargetCode, "text") + + EstQuantity.targetSelect(project, row, "manual", index), field("quantities", index, "formula", row.formula, "text"), field("quantities", index, "digits", row.digits ?? 2), esc(row.Error || number(row.GN)), diff --git a/tools/est-test/native-forms.js b/tools/est-test/native-forms.js index d68cb0e5a..100c39b88 100644 --- a/tools/est-test/native-forms.js +++ b/tools/est-test/native-forms.js @@ -96,11 +96,19 @@ const EstForms = (() => { }; } if (id === 14) { + const target = (item) => { + if (!item.TargetKey) return item.TargetCode ?? item.LinkCode ?? ""; + const row = project.tables.EstMainDat.find( + (entry) => entry.PkID != null && `id:${entry.PkID}` === item.TargetKey, + ); + return row ? `${row.Name || "내역"} (${row.Code || item.TargetKey})` : item.TargetKey; + }; const manual = EstQuantity.manual(project.quantities || []), rows = manual.rows.map((row) => ({ ...row, SheetName: "수식표", formula: row.formula || "", + TargetCode: target(row), })), errors = [...manual.errors]; for (const sheet of project.quantitySheets || []) { @@ -112,7 +120,7 @@ const EstForms = (() => { ...item, SheetName: sheet.Name || sheet.Code, formula: index < 0 ? "" : EstText.clean(calculated.rows[index].LineData), - TargetCode: item.TargetCode ?? item.LinkCode ?? "", + TargetCode: target(item), }); } } diff --git a/tools/est-test/native-quantity.cjs b/tools/est-test/native-quantity.cjs index d5c1116f1..3abea1d08 100644 --- a/tools/est-test/native-quantity.cjs +++ b/tools/est-test/native-quantity.cjs @@ -151,18 +151,34 @@ const EstQuantity = (() => { const quantities = new Map(); for (const item of items) { const code = item.TargetCode ?? item.LinkCode; - if (!code) continue; - const targets = project.tables.EstMainDat.filter((r) => r.Code === code && !Number(r.Level)); - if (!targets.length) throw Error(`연결 내역코드 없음: ${code}`); - if (targets.length > 1) throw Error(`연결 내역코드 중복: ${code}`); - quantities.set(code, (quantities.get(code) || 0) + Number(item.GN || 0)); + if (!code && !item.TargetKey) continue; + const targets = project.tables.EstMainDat.map((row, index) => ({ row, index })).filter( + ({ row }) => + !Number(row.Level) && + (item.TargetKey + ? row.PkID != null && `id:${row.PkID}` === item.TargetKey + : row.Code === code), + ); + if (!targets.length) throw Error(`연결 내역 없음: ${item.TargetKey || code}`); + if (targets.length > 1) throw Error(`연결 내역 중복: ${item.TargetKey || code}`); + const index = targets[0].index; + quantities.set(index, (quantities.get(index) || 0) + Number(item.GN || 0)); } if (!quantities.size) throw Error("연결할 내역코드를 먼저 입력하세요."); return quantities; } function applyTotals(project, quantities) { - for (const row of project.tables.EstMainDat) - if (quantities.has(row.Code) && !Number(row.Level)) row.GN = quantities.get(row.Code); + for (const [index, value] of quantities) project.tables.EstMainDat[index].GN = value; + } + function targetSelect(project, item, section, index) { + const { esc } = EstApp; + const rows = project.tables.EstMainDat.filter((row) => row.PkID != null && !Number(row.Level)); + return ``; } function newCode(items) { const codes = new Set(items.map((row) => row.Code)); @@ -186,7 +202,7 @@ const EstQuantity = (() => { { esc, number, byId } = EstApp; byId("work-content").innerHTML = selector(state) + - `
${output.items.map((item, index) => ``).join("")}
산출코드품명규격저장 수량재계산 수량연결 내역코드
${esc(item.Code)}${number(sheet.items[index].GN)}${number(item.GN)}

${esc(output.errors.join(" / "))} 산출 행의 결과 표지로 수량 항목을 연결합니다. 신규 산출서와 행 변경은 JSON으로 저장합니다.

${sheet.rows.map((row, index) => ``).join("")}
줄산출근거 편집작업
${index + 1}
`; + `
${output.items.map((item, index) => ``).join("")}
산출코드품명규격저장 수량재계산 수량연결 내역코드
${esc(item.Code)}${number(sheet.items[index].GN)}${number(item.GN)}${targetSelect(state.project, item, "sheet", index)}

${esc(output.errors.join(" / "))} 산출 행의 결과 표지로 수량 항목을 연결합니다. 신규 산출서와 행 변경은 JSON으로 저장합니다.

${sheet.rows.map((row, index) => ``).join("")}
줄산출근거 편집작업
${index + 1}
`; } function install() { document.addEventListener("change", (event) => { @@ -197,6 +213,23 @@ const EstQuantity = (() => { EstApp.render(); return; } + if (node.dataset.quantitySelect !== undefined) { + const item = + node.dataset.quantitySelect === "manual" + ? state.project.quantities[Number(node.dataset.index)] + : state.project.quantitySheets?.find((s) => s.Code === state.quantitySheet)?.items[ + Number(node.dataset.index) + ]; + if (!item) return; + const row = state.project.tables.EstMainDat.find( + (r) => r.PkID != null && `id:${r.PkID}` === node.value && !Number(r.Level), + ); + EstApp.change(() => { + item.TargetKey = row ? node.value : ""; + item.TargetCode = row?.Code || ""; + }, false); + return; + } const sheet = state.project.quantitySheets?.find((s) => s.Code === state.quantitySheet); if (!sheet) return; if (node.dataset.quantityName !== undefined) @@ -213,10 +246,11 @@ const EstQuantity = (() => { false, ); if (node.dataset.quantityTarget !== undefined) - EstApp.change( - () => (sheet.items[Number(node.dataset.quantityTarget)].TargetCode = node.value), - false, - ); + EstApp.change(() => { + const item = sheet.items[Number(node.dataset.quantityTarget)]; + item.TargetCode = node.value; + item.TargetKey = ""; + }, false); }); document.addEventListener("click", (event) => { const { state } = EstApp; @@ -298,6 +332,7 @@ const EstQuantity = (() => { manual, totals, applyTotals, + targetSelect, newCode, selector, render,