Files
Aislo/tools/est-test/native-misc-tools.js
T
2026-11-12 18:49:51 +09:00

320 lines
12 KiB
JavaScript

// 잡다한 원본 대화(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);
},
);
}
// 인쇄양식 불러오기·저장하기(mnuPrintFormatLoad·Save 0x10ed318·0x10ed000): .pfot = 인쇄정보표 한 벌(원본 이진 그대로).
// 불러오면 공사 인쇄표(printCatalog)를 그 값으로, 저장은 지금 인쇄표(불러온 원본 행 우선)를 .pfot 로. .gfot(격자 배치)는 다루지 않음.
async function pfot(name, state) {
if (name === "pfot-load") {
const file = await new Promise((resolve) => {
const picker = document.createElement("input");
picker.type = "file";
picker.accept = ".pfot";
picker.addEventListener("change", () => resolve(picker.files[0]));
picker.click();
});
if (!file) return;
const rows = await EstApp.api("/api/pfot-read", {
method: "POST",
body: await file.arrayBuffer(),
});
EstApp.change((project) => {
project.printFormat = rows;
project.printCatalog = rows.map((r) =>
Object.fromEntries(Object.entries(r).filter(([k, v]) => k !== "__nulls" && !v?.binary)),
);
}, false);
state.notice = `인쇄양식 「${file.name}」(서식 ${rows.length}개)을 불러왔습니다.`;
} else {
const defaults = await EstApp.api("/api/print-catalog"),
saved = new Map((state.project.printCatalog || []).map((r) => [r.Index, r])),
full = new Map((state.project.printFormat || []).map((r) => [r.Index, r]));
const rows = defaults.map((r) => ({
...r,
...full.get(r.Index),
...saved.get(r.Index),
__nulls: full.get(r.Index)?.__nulls,
}));
const response = await fetch("/api/pfot-write", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rows }),
});
if (!response.ok) throw Error((await response.json()).error);
EstApp.download(
`${state.project.name || "인쇄양식"}.pfot`,
await response.blob(),
"application/octet-stream",
);
}
EstApp.render();
}
// 사용위치 검색 창(TfrmUsesDataSerch btnSerch·btnAllDisplay): 현재 표 항목을 품명·규격(빈칸·|·괄호 무시)으로 거르고 골라 사용위치 보기
function usesList(state, word) {
const rows = state.project.tables[state.view];
if (!rows) throw Error("자료 화면에서 쓰세요.");
const norm = (x) => String(x ?? "").replace(/[\s|()]/g, ""),
w = norm(word),
{ esc } = EstApp;
const list = rows
.map((r, i) => [r, i])
.filter(
([r]) => (r.Name || r.Code) && (!w || norm(r.Name).includes(w) || norm(r.Size).includes(w)),
)
.slice(0, 300);
EstApp.showDialog(
"데이터 사용위치 검색",
`<div class="business-tools"><input data-uses-word value="${esc(word || "")}" placeholder="품명·규격"><button data-action="uses-list-search">검 색</button><button data-action="uses-list-all">전체보기</button></div><div style="max-height:420px;overflow:auto"><table class="sheet"><tbody>${list.map(([r, i]) => `<tr><td>${esc(r.Code ?? "")}</td><td style="text-align:left">${esc(r.Name ?? "")}</td><td>${esc(r.Size ?? "")}</td><td><button data-action="uses-list-pick" data-i="${i}">사용위치</button></td></tr>`).join("")}</tbody></table></div>`,
);
}
function handle(name, state, node) {
if (name === "uses-list" || name === "uses-list-all") {
usesList(state, "");
return true;
}
if (name === "uses-list-search") {
usesList(state, document.querySelector("[data-uses-word]")?.value || "");
return true;
}
if (name === "uses-list-pick") {
EstExtra.showUsesSearchDialog(state.project.tables[state.view][Number(node.dataset.i)]);
return true;
}
if (name === "pfot-load" || name === "pfot-save") {
pfot(name, state).catch((error) => {
state.notice = error.message;
EstApp.render();
});
return true;
}
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 };
})();