feat(est-test): 전회기성 불러오기·조달수수료 입력·원가 내역 금액 넣기·구성비 표시
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -489,6 +489,8 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD
|
||||
- 남음: 구성비·요율 전환(CalcBase), 약어 표시(SName), 제비율 기준 문서 보기(원본 한글 파일).
|
||||
- [x] 수량산출서 검색·전체보기(원본 TFRMMULLANG btnSerch·btnAllDisplay — 빈칸·|·괄호 무시 포함 검색) — 고르기 줄을 `native-quantity-sheets.js` 로 옮겨 700줄 제한 유지.
|
||||
- 검증완료: `test_est_quantity_sheets.py` 검색 시험 포함 수량 시험 22개 통과. ORCA 브라우저에서 「집수」 검색 → 항목에 집수정이 있는 「배수」만·선택 이동, 전체보기 → 3개 복귀, 샘플 복구.
|
||||
- [x] 전회기성 불러오기·조달수수료·원가에 내역 금액·원가 구성비(spec_pb1 1·9·10 · spec_pb2 8) — 전회기성: 완료 회차 아무것이나 골라 그 누계를 전회로(현재 회차 거부, `round.junFrom`). 조달수수료 입력(Ctrl+J): 현재 줄을 Code AJJ01·LinkIndex 57·GN %·자리방법 Index·자리수 Gangub·계산 비목 OCheck·저장 칸 SCheck 로(57 줄이면 수정, 허용 검사·원본 문구). 원가계산서에 내역 금액 넣기: 원본은 끌어 놓기 — 웹은 줄 고르기 + 합계/노무/재료/경비 고르기 → 원가 줄 Cost 고정(LinkIndex 1, 덮어쓰기 확인). 구성비 표시: 원가 요율 칸을 「비율」로 바꿔 기준 행(CalcBase 또는 도급액) 대비 대상 비목 Cost 를 0.01% 절사·최대 나머지로 100% 맞춤(계산 불변).
|
||||
- 검증완료: 기성·바인딩 시험 14개 통과. ORCA 브라우저에서 구성비 20줄 합 100.00%·머리 「비율」, 내역 금액 넣기 노무비 384,804, Ctrl+J 조달수수료 AJJ01·57·0.54%·절하, 샘플 복구.
|
||||
- [x] 단가 도구(spec_pb2) — 중복 항목 정리(자재단가: 품명·규격·단위 빈칸·| 무시 같으면 TC 가장 큰 행만, 같으면 뒤 행, 묻지 않음), 단가정의 엑셀변환(표준시장단가 설명문 있는 항목만 「코드 / 번호. 품명 (규격) [단위]」+설명 줄, .xlsx), 할증·낙찰(고른 2행 이상 또는 전 행 Ratio·NachalRatio, 원래 단가 OTC 보관, 100 이면 자리 해제)·복원(두 율 0), 바꾸기(Main Data 로 현재 표 하나만 갱신, 못 찾음 표시), SUM(현재 줄을 항목선택계산 62 줄로: Code 'A'+두 자리 색인·적용율·비목, 원본 거부 문구, 줄 자신 SKind = 묶음 최대+1). 엔진: 62 줄 Code 를 두 자리 색인 번호로 읽음(설치 main.estx 의 「A01」 꼴, 예전 글자 해석은 A0103 의 C 를 놓쳤음).
|
||||
- 검증완료: `test_est_price_tools.py` 2개(중복 2개 지움·단가정의 줄·율 쓰기 범위·SUM Code A0103·거부 4종, 엔진 A0103 → 색인 1·3 합 150×10% = 15) 및 REA 대조 39개 통과. ORCA 브라우저에서 보통인부 낙찰 90% 172,068→154,861, 복원 →172,068, 바꾸기(Main) → 2026 하반기 값, 샘플 복구.
|
||||
- [x] 워드형 수량산출서 표와 편집 도구(원본 TFRMMULWORD·TFRMMULLANG — spec_word · spec_pa) — 가져오기가 WPT <table> 을 통째로 버리던 결함 고침: 표를 한 줄 { table: [[칸]] }(글·수량 표지 r/L/G·품명/규격 표지·수식·세로병합)로 보존(설치 main.estx 표 5개 9×13~5×5). 문서 읽기는 `native-quantity-wpt.cjs` 로 분리. 표 화면: 칸 고르기(Shift 범위)·글/수식 고치기·방향 셀 합(우·좌·위·아래, 원본 범위 규칙·1,234.5 꼴)·수량으로 지정/해제·셀수직병합/분리·표 모양(테두리)·아래 표 만들기. 줄 도구: 문단(정렬·들여쓰기)·그림 넣기·한페이지 삽입/삭제(웹 한 쪽 45줄)·품명지정/규격지정(한 줄 하나·40자·제목 위 금지·원본 문구)·테두리선·산출근거 양식(화면 표시)·공종명칭 입력(머리 제목 줄 굵게 큰 글자). 수량 불러오기(Main Data 산출서 75개에서 새 산출서로), 수량목록보기(모든 산출서·수량 항목·원문 미리보기·검색·선택수량복사), 내역서 수량붙여넣기(연결)(고른 줄부터 아래로 수량·연결, 품명,규격입력 체크 시 이름도). 표·서식을 고친 산출서는 ESTX 저장 거부(JSON), 고치지 않은 표 산출서는 ESTX 저장이 표 줄을 건너 문단만 맞춤.
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<script defer src="native-quantity-table.js"></script>
|
||||
<script defer src="native-quantity-list.js"></script>
|
||||
<script defer src="native-price-tools.js"></script>
|
||||
<script defer src="native-misc-tools.js"></script>
|
||||
<script defer src="native-actions-more.js"></script>
|
||||
<script defer src="native-wonga.js"></script>
|
||||
<script defer src="native-jungsang.js"></script>
|
||||
@@ -159,7 +160,11 @@
|
||||
><button data-action="cost-part">비목별 단가 불러오기(일위대가)</button
|
||||
><button data-action="global-var">전역변수 지정,해제</button
|
||||
><button data-action="global-var-list">전역변수 목록</button
|
||||
><button data-action="data-update">현재데이타 갱신</button>
|
||||
><button data-action="data-update">현재데이타 갱신</button
|
||||
><button data-action="jodal-fee">조달수수료 입력(Ctrl+J)</button
|
||||
><button data-action="cost-to-wonga">원가계산서에 내역 금액 넣기</button
|
||||
><button data-action="wonga-biul-mode">원가 구성비·요율 표시 전환</button
|
||||
><button data-action="jun-load">전회기성 불러오기</button>
|
||||
<hr />
|
||||
<button data-action="find">찾기·다음찾기(Ctrl+F)</button>
|
||||
<button data-action="multi-search">통합 검색(저장된 다른 작업)</button>
|
||||
|
||||
@@ -4,6 +4,7 @@ const EstActionsMore = (() => {
|
||||
[
|
||||
typeof EstQuantityList !== "undefined" && EstQuantityList,
|
||||
typeof EstPriceTools !== "undefined" && EstPriceTools,
|
||||
typeof EstMiscTools !== "undefined" && EstMiscTools,
|
||||
].filter(Boolean);
|
||||
function handle(name, state, node) {
|
||||
for (const module of modules()) if (module.handle(name, state, node)) return true;
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
// 잡다한 원본 대화(spec_pb1·pb2): 전회기성 불러오기(TfrmAccomJunLoad — 완료 회차 아무것이나 전회로), 조달수수료 입력(TfrmJodalBiul, Ctrl+J),
|
||||
// 원가계산에 내역 금액 넣기(TfrmEstCostToWonga — 끌어 놓기 대신 줄 고르기), 원가 구성비 모드(popupBiulMode — 표시 전용 「비율」 열).
|
||||
const EstMiscTools = (() => {
|
||||
const num = (v) => Number(v) || 0;
|
||||
const plain = (s) => String(s ?? "").replace(/\s/g, "");
|
||||
|
||||
// 구성비(ca19d8·ca06c4): 기준 행(CalcBase, 없으면 도급액 줄) 대비 대상 비목 Cost 비율, 0.01% 절사 뒤 최대 나머지로 100% 맞춤
|
||||
function biul(rows) {
|
||||
let base = rows
|
||||
.map((r, i) => (r.CalcBase ? i : -1))
|
||||
.filter((i) => i >= 0)
|
||||
.at(-1);
|
||||
if (base == null)
|
||||
base = rows.findIndex((r) => num(r.LinkIndex) === 55 || /도급액/.test(plain(r.Name)));
|
||||
if (base < 0 || !num(rows[base].Cost)) return null;
|
||||
const total = num(rows[base].Cost),
|
||||
target = new Set();
|
||||
let sum = 0;
|
||||
rows.slice(0, base).forEach((r, i) => {
|
||||
if (!num(r.Cost) || num(r.CalcIndex) >= 2) return;
|
||||
const end = num(r.EndLineIndex);
|
||||
if (i === 0 || end === 0 || end === 3 || (end === 1 && String(r.Youl ?? "").includes("%"))) {
|
||||
target.add(i);
|
||||
sum += num(r.Cost);
|
||||
}
|
||||
});
|
||||
for (let i = base - 1; i >= 0 && sum < total; i--) {
|
||||
const r = rows[i];
|
||||
if (
|
||||
num(r.Cost) &&
|
||||
!target.has(i) &&
|
||||
num(r.EndLineIndex) === 1 &&
|
||||
!String(r.VSusic ?? "").trim()
|
||||
) {
|
||||
target.add(i);
|
||||
sum += num(r.Cost);
|
||||
}
|
||||
}
|
||||
const exact = new Map([...target].map((i) => [i, num(rows[i].Cost) / total])),
|
||||
cut = new Map([...exact].map(([i, v]) => [i, Math.trunc(v * 10000) / 10000]));
|
||||
let t = [...cut.values()].reduce((a, b) => a + b, 0);
|
||||
const bumped = new Set();
|
||||
for (let n = Math.round((1 - t) * 10000); n > 0 && t < 1; n--) {
|
||||
const pick = [...exact.keys()]
|
||||
.filter((i) => !bumped.has(i))
|
||||
.sort((a, b) => exact.get(b) - cut.get(b) - (exact.get(a) - cut.get(a)))[0];
|
||||
if (pick == null) break;
|
||||
cut.set(pick, cut.get(pick) + 0.0001);
|
||||
bumped.add(pick);
|
||||
t += 0.0001;
|
||||
}
|
||||
return rows.map((_, i) =>
|
||||
i === base ? "100%" : cut.has(i) ? `${(cut.get(i) * 100).toFixed(2)}%` : "",
|
||||
);
|
||||
}
|
||||
|
||||
function jodal(state) {
|
||||
if (state.view !== "EstMainDat") throw Error("내역서에서 쓰세요.");
|
||||
const rows = state.project.tables.EstMainDat,
|
||||
at = [...state.selection].sort((a, b) => a - b)[0],
|
||||
row = rows[at];
|
||||
if (!row) throw Error("조달수수료를 넣을 줄을 고르세요.");
|
||||
if (
|
||||
(num(row.Level) > 0 && plain(row.Name) && !plain(row.Name).includes("수수")) ||
|
||||
![0, 57, 64].includes(num(row.LinkIndex))
|
||||
)
|
||||
throw Error("현재 라인에는 조달수수료를 입력할수 없습니다.");
|
||||
const edit = num(row.LinkIndex) === 57;
|
||||
EstActions.form(
|
||||
"조달수수료",
|
||||
[
|
||||
{ name: "name", label: "명 칭", value: edit ? row.Name : "조달수수료" },
|
||||
{ name: "biul", label: "비 율(%)", type: "number", value: edit ? num(row.GN) : 0.54 },
|
||||
{
|
||||
name: "o",
|
||||
label: "계산할 비목",
|
||||
value: edit ? num(row.OCheck) || 1 : 1,
|
||||
choices: [
|
||||
[1, "재료비의"],
|
||||
[2, "노무비의"],
|
||||
[3, "경비의"],
|
||||
[4, "합계의"],
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "s",
|
||||
label: "계산결과 저장위치",
|
||||
value: edit ? num(row.SCheck) || 1 : 1,
|
||||
choices: [
|
||||
[1, "재료비에"],
|
||||
[3, "경비에"],
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "m",
|
||||
label: "자리수 방법",
|
||||
value: edit ? num(row.Index) || 2 : 2,
|
||||
choices: [
|
||||
[1, "사사오입"],
|
||||
[2, "절하"],
|
||||
[3, "절상"],
|
||||
],
|
||||
},
|
||||
{ name: "d", label: "자리수", type: "number", value: edit ? num(row.Gangub) : 0 },
|
||||
],
|
||||
(v) => {
|
||||
if (!Number.isFinite(v.biul)) throw Error("비율은 수치로 입력하세요.");
|
||||
EstApp.change((project) =>
|
||||
Object.assign(project.tables.EstMainDat[at], {
|
||||
Code: "AJJ01",
|
||||
Name: String(v.name).trim(),
|
||||
GN: v.biul,
|
||||
Unite: "%",
|
||||
LinkIndex: 57,
|
||||
Index: Number(v.m),
|
||||
Gangub: v.d,
|
||||
OCheck: Number(v.o),
|
||||
SCheck: Number(v.s),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function costToWonga(state) {
|
||||
if (state.view !== "Wonga") throw Error("원가계산서에서 넣을 줄을 고르세요.");
|
||||
const at = [...state.selection].sort((a, b) => a - b)[0],
|
||||
target = state.project.tables.Wonga[at];
|
||||
if (!target) throw Error("원가계산서에서 넣을 줄을 고르세요.");
|
||||
const est = state.project.tables.EstMainDat;
|
||||
EstActions.form(
|
||||
"원가계산에 입력할 비목선택",
|
||||
[
|
||||
{
|
||||
name: "row",
|
||||
label: "내역 줄",
|
||||
value: "",
|
||||
choices: est
|
||||
.map((r, i) => [i, `${i + 1}. ${r.Name || r.Code || ""} ${num(r.TC).toLocaleString()}`])
|
||||
.filter(([i]) => est[i].Name || est[i].Code),
|
||||
},
|
||||
{
|
||||
name: "kind",
|
||||
label: "입력할 비목",
|
||||
value: -5,
|
||||
choices: [
|
||||
[-5, "합계 금액"],
|
||||
[-1, "노무비 금액"],
|
||||
[-2, "재료비 금액"],
|
||||
[-3, "경비 금액"],
|
||||
],
|
||||
},
|
||||
],
|
||||
(v) => {
|
||||
const src = est[Number(v.row)];
|
||||
if (!src) throw Error("내역 줄을 고르세요.");
|
||||
if (plain(target.Name) && !confirm(`${target.Name}에 덮어쓰시겠습니까?`)) return;
|
||||
const field = { "-5": "TC", "-1": "NC", "-2": "GC", "-3": "JC" }[v.kind];
|
||||
EstApp.change((project) =>
|
||||
Object.assign(project.tables.Wonga[at], {
|
||||
CalcIndex: 0,
|
||||
LinkIndex: 1,
|
||||
Youl: "",
|
||||
VSusic: "",
|
||||
BaseField: "",
|
||||
Name: src.Name,
|
||||
TC: num(src.TC),
|
||||
NC: num(src.NC),
|
||||
GC: num(src.GC),
|
||||
JC: num(src.JC),
|
||||
SC: num(src.SC),
|
||||
OCheck: Number(v.kind),
|
||||
Cost: num(src[field]),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function junLoad(state) {
|
||||
const project = state.project,
|
||||
round = project.progress?.find((r) => r.id === state.roundId);
|
||||
if (!round) throw Error("기성 회차를 고르세요.");
|
||||
const done = project.progress.filter((r) => r.locked && r.result);
|
||||
if (!done.length) throw Error("전회없슴");
|
||||
EstActions.form(
|
||||
"전회기성 불러오기",
|
||||
[
|
||||
{
|
||||
name: "id",
|
||||
label: "불러올 차수선택",
|
||||
value: round.junFrom || "",
|
||||
choices: done.map((r) => [r.id, `${r.name || r.id}`]),
|
||||
},
|
||||
],
|
||||
(v) => {
|
||||
if (v.id === round.id) throw Error("현재작업중인 회차를 전회로 불러올수 없습니다.");
|
||||
EstApp.change((p) => (p.progress.find((r) => r.id === round.id).junFrom = v.id), false);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function handle(name, state) {
|
||||
if (name === "jodal-fee") jodal(state);
|
||||
else if (name === "cost-to-wonga") costToWonga(state);
|
||||
else if (name === "jun-load") junLoad(state);
|
||||
else if (name === "wonga-biul-mode")
|
||||
EstApp.change(
|
||||
(project) => (project.options.WongaBiulMode = !project.options.WongaBiulMode),
|
||||
false,
|
||||
);
|
||||
else return false;
|
||||
return true;
|
||||
}
|
||||
if (typeof document !== "undefined" && document.addEventListener)
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (
|
||||
event.ctrlKey &&
|
||||
!event.altKey &&
|
||||
event.key.toLowerCase() === "j" &&
|
||||
EstApp.state.view === "EstMainDat"
|
||||
) {
|
||||
event.preventDefault();
|
||||
try {
|
||||
jodal(EstApp.state);
|
||||
} catch (error) {
|
||||
EstApp.state.notice = error.message;
|
||||
EstApp.render();
|
||||
}
|
||||
}
|
||||
});
|
||||
return { biul, handle };
|
||||
})();
|
||||
@@ -42,12 +42,17 @@ const EstProgress = (() => {
|
||||
const ambiguous = ambiguousKeys(rows);
|
||||
const earlier = project.progress.slice(0, position);
|
||||
if (earlier.some((item) => !item.locked)) errors.push("전회 기성 회차를 먼저 확정하세요.");
|
||||
const previous = earlier.at(-1)?.result,
|
||||
// 전회기성 불러오기(TfrmAccomJunLoad): 고른 완료 회차가 있으면 그 누계를 전회로
|
||||
const chosen =
|
||||
round.junFrom &&
|
||||
project.progress.find((r) => r.id === round.junFrom && r.locked && r !== round),
|
||||
prior = chosen || earlier.at(-1);
|
||||
const previous = prior?.result,
|
||||
oldRows = new Map((previous?.rows || []).map((row, index) => [key(row, index), row]));
|
||||
if (earlier.at(-1)?.locked && !Array.isArray(previous?.rows))
|
||||
if (prior?.locked && !Array.isArray(previous?.rows))
|
||||
errors.push("확정된 전회 기성 결과가 없습니다. 전회 결과를 확인하세요.");
|
||||
if (
|
||||
earlier.at(-1)?.locked &&
|
||||
prior?.locked &&
|
||||
source.wonga?.length &&
|
||||
!Array.isArray(previous?.costs) &&
|
||||
!Array.isArray(previous?.wonga)
|
||||
|
||||
@@ -80,13 +80,19 @@ const EstViews = (() => {
|
||||
}
|
||||
function grid(table, list, columns) {
|
||||
const { esc, number, state } = EstApp;
|
||||
const biul =
|
||||
table === "Wonga" &&
|
||||
state.project.options.WongaBiulMode &&
|
||||
typeof EstMiscTools !== "undefined"
|
||||
? EstMiscTools.biul(state.project.tables.Wonga || [])
|
||||
: null;
|
||||
const abbr =
|
||||
table === "Wonga" &&
|
||||
state.project.options.WongaSNameShow &&
|
||||
typeof EstWongaTools !== "undefined"
|
||||
? EstWongaTools.abbreviate(state.project.tables.Wonga || [], state.project.options)
|
||||
: null;
|
||||
return `<table class="sheet" data-grid="${table}"><thead><tr><th class="row-number">선택</th>${columns.map((key) => `<th class="col-${key}">${esc((table === "Wonga" && typeof EstJungSang !== "undefined" && EstJungSang.label(state.project, key)) || (typeof EstSources !== "undefined" && EstSources.label(state.project.options, table, key)) || EstModel.labels[key] || key)}</th>`).join("")}</tr></thead><tbody>${list.map(({ row, index }) => `<tr data-index="${index}" class="${state.selection.has(index) ? "selected " : ""}${Number(row.Index) > 0 ? "heading " : ""}${Number(row.Level) > 0 ? "level " : ""}${row.Error ? "error-row" : ""}"><th class="row-number"><label><input type="checkbox" data-select="${index}" ${state.selection.has(index) ? "checked" : ""}>${index + 1}</label></th>${columns.map((key) => `<td class="col-${key}"${key === "Name" && Number(row.LeftSpace) > 0 ? ` style="padding-left:${Number(row.LeftSpace) * 6}px"` : ""}>${abbr && key === "VSusic" ? `<span title="산출근거 약어 표시 중(편집 잠김)">${esc(abbr[index])}</span>` : cell(table, index, key, row[key])}</td>`).join("")}</tr>`).join("")}</tbody></table>`;
|
||||
return `<table class="sheet" data-grid="${table}"><thead><tr><th class="row-number">선택</th>${columns.map((key) => `<th class="col-${key}">${esc((biul && key === "Youl" && "비율") || (table === "Wonga" && typeof EstJungSang !== "undefined" && EstJungSang.label(state.project, key)) || (typeof EstSources !== "undefined" && EstSources.label(state.project.options, table, key)) || EstModel.labels[key] || key)}</th>`).join("")}</tr></thead><tbody>${list.map(({ row, index }) => `<tr data-index="${index}" class="${state.selection.has(index) ? "selected " : ""}${Number(row.Index) > 0 ? "heading " : ""}${Number(row.Level) > 0 ? "level " : ""}${row.Error ? "error-row" : ""}"><th class="row-number"><label><input type="checkbox" data-select="${index}" ${state.selection.has(index) ? "checked" : ""}>${index + 1}</label></th>${columns.map((key) => `<td class="col-${key}"${key === "Name" && Number(row.LeftSpace) > 0 ? ` style="padding-left:${Number(row.LeftSpace) * 6}px"` : ""}>${biul && key === "Youl" ? `<span title="구성비(표시 전용)">${esc(biul[index])}</span>` : abbr && key === "VSusic" ? `<span title="산출근거 약어 표시 중(편집 잠김)">${esc(abbr[index])}</span>` : cell(table, index, key, row[key])}</td>`).join("")}</tr>`).join("")}</tbody></table>`;
|
||||
}
|
||||
function render(state) {
|
||||
const { project, view } = state,
|
||||
|
||||
@@ -98,6 +98,7 @@ const allowed = new Set([
|
||||
"native-quantity-table.js",
|
||||
"native-quantity-list.js",
|
||||
"native-price-tools.js",
|
||||
"native-misc-tools.js",
|
||||
"native-actions-more.js",
|
||||
"native-wonga-tools.js",
|
||||
"native-estimate-print.js",
|
||||
|
||||
Reference in New Issue
Block a user