Files
Aislo/tools/est-test/native-name-reg.js
T
eomsangdonandClaude Opus 5.5 f7b68be72a feat(est-test): 품명정리와 자재·견적단가 등록
- 원본 처리 근거대로 커서 자리 품명 나누기, 삭제할 칸, 빈 단위만 채움
- 내역·일위대가 행을 새 자재(M)·견적(W) 단가로 등록하고 연결, 중복·호표 거부

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 19:35:03 +09:00

132 lines
7.0 KiB
JavaScript

// 원본 품명정리(TfrmHead ActionNameDeivde 0xf5a2b4·TfrmNameDevide 적용 0xf595c0·규격으로지정 0xf59640)와
// 자재단가·견적단가 등록(popMainStuffcReg·popMainEstimateReg → c_f00264·c_f00670).
// 품명정리: 기존명칭에서 고른 자리 앞 = 품명(다듬지 않음), 뒤에서 「삭제할 칸」만큼 떼고 기존 규격을 붙여 다듬은 값 = 규격,
// 단위는 비어 있을 때만 채움. 고른 일위대가 호표마다 같은 자리로 나누고 HeadTitleDat 의 같은 코드도 맞춘다.
// 등록: 같은 품명·규격(빈칸·| 무시) 있으면 거부, 코드 = 머리글자(M·W) + PkID 10자리(쓰인 코드면 하나씩 올림),
// 자재 Cost1 = TC = 행 재료단가 · 견적 NC/GC/JC = 행 노무/재료/경비단가, 원래 행은 새 코드·연결(5·101)로 바꾼다.
const EstNameReg = (() => {
const num = (value) => Number(value) || 0;
const strip = (value) => String(value ?? "").replace(/[\s|]/g, "");
function divide(project, indices, position, cut, unit) {
if (!Number.isInteger(position) || position < 0) throw Error("나눌 자리를 고르세요.");
if (!Number.isInteger(cut) || cut < 0) throw Error("삭제할 칸은 0 이상의 정수입니다.");
const rows = project.tables.HeadMainDat,
titles = project.tables.HeadTitleDat || [];
const picked = indices.filter((i) => num(rows[i]?.Index) > 0);
if (!picked.length) throw Error("품명을 정리할 일위대가 호표(제목 줄)를 고르세요.");
for (const i of picked) {
const row = rows[i],
name = String(row.Name ?? "");
const rest = name.slice(position).slice(cut);
row.Name = name.slice(0, position);
row.Size = `${rest}${row.Size ?? ""}`.trim();
if (!strip(row.Unite) && unit) row.Unite = unit;
const title = titles.find((t) => t.Code === row.Code);
if (title) Object.assign(title, { Name: row.Name, Size: row.Size, Unite: row.Unite });
}
return picked.length;
}
const targets = {
stuff: { table: "StuffcDat", kind: 5, letter: "M", label: "자재단가" },
estimate: { table: "EstiMateDat", kind: 101, letter: "W", label: "견적단가" },
};
function register(project, view, index, which) {
const target = targets[which],
row = project.tables[view]?.[index];
if (!target || !row) throw Error("등록할 행을 고르세요.");
if (view === "HeadMainDat" && num(row.Index) > 0)
throw Error(`호표는 ${target.label}에 등록할수 없습니다.`);
if (!String(row.Name ?? "").trim()) throw Error("명칭을 입력하세요.");
const rows = (project.tables[target.table] ||= []);
const same = rows.find(
(r) => strip(r.Name) === strip(row.Name) && strip(r.Size) === strip(row.Size),
);
if (same)
throw Error(`같은 명칭의 데이타가 있습니다 . ${target.label} (${num(same.Index)} 번)`);
let pk = Math.max(0, ...rows.map((r) => num(r.PkID))) + 1;
const used = new Set(rows.map((r) => r.Code));
let code = `${target.letter}${String(pk).padStart(10, "0").slice(-10)}`;
while (used.has(code)) code = `${target.letter}${String(++pk).padStart(10, "0").slice(-10)}`;
const schema = rows[0] ? Object.keys(rows[0]) : ["Name", "Size", "Unite"];
const record = {};
for (const key of schema)
if (!["PkID", "Index", "Code"].includes(key) && Object.hasOwn(row, key))
record[key] = row[key];
Object.assign(record, { PkID: pk, Code: code, Index: rows.length + 1 });
if (which === "stuff")
Object.assign(record, { Cost1: num(row.GU), TC: num(row.GU), OTC: num(row.GU) });
else {
const [nc, gc, jc] = [num(row.NU), num(row.GU), num(row.JU)];
Object.assign(record, {
NC: nc,
GC: gc,
JC: jc,
TC: nc + gc + jc,
ONC: nc,
OGC: gc,
OJC: jc,
OTC: nc + gc + jc,
});
}
rows.push(record);
row.Code = code;
row.LinkIndex = target.kind;
return code;
}
function divideDialog(state) {
if (state.view !== "HeadMainDat") throw Error("일위대가 화면에서 품명을 정리하세요.");
const rows = state.project.tables.HeadMainDat,
picked = [...state.selection].sort((a, b) => a - b).filter((i) => num(rows[i]?.Index) > 0);
if (!picked.length) throw Error("품명을 정리할 일위대가 호표(제목 줄)를 고르세요.");
const { esc } = EstApp;
EstApp.showDialog(
"품명정리",
`<p>기존명칭에서 품명과 규격을 나눌 자리에 커서를 두고 「규격으로 지정」을 누르세요. 고른 ${picked.length}개 호표에 같은 자리로 적용합니다.</p><div class="form-fields"><label>기존명칭<input name="nd-all" value="${esc(rows[picked[0]].Name ?? "")}"></label><label>명칭<input name="nd-name" readonly></label><label>규격<input name="nd-size" readonly></label><label>규격에서 삭제할 칸<input name="nd-cut" type="number" value="0"></label><label>단위(비어 있을 때만)<input name="nd-unit"></label></div><button data-action="name-divide-set">규격으로 지정</button> <button data-action="name-divide-apply">적용</button>`,
);
state.nameDivide = { picked, position: null };
}
function field(name) {
return document.querySelector(`#dialog-content [name="${name}"]`);
}
function handle(name, state) {
if (name === "name-divide") divideDialog(state);
else if (name === "name-divide-set") {
const all = field("nd-all"),
position = all.selectionStart ?? 0;
state.nameDivide.position = position;
field("nd-name").value = all.value.slice(0, position);
field("nd-size").value = all.value.slice(position);
} else if (name === "name-divide-apply") {
const info = state.nameDivide;
if (info?.position == null) throw Error("먼저 「규격으로 지정」으로 나눌 자리를 정하세요.");
const cut = Number(field("nd-cut").value),
unit = field("nd-unit").value.trim();
EstApp.byId("dialog").close();
let count;
EstApp.change((project) => (count = divide(project, info.picked, info.position, cut, unit)));
state.notice = `${count}개 호표의 품명을 정리했습니다.`;
} else if (name === "register-stuff" || name === "register-estimate") {
const which = name === "register-stuff" ? "stuff" : "estimate",
index = [...state.selection][0],
row = state.project.tables[state.view]?.[index];
if (!["EstMainDat", "HeadMainDat"].includes(state.view) || state.selection.size !== 1 || !row)
throw Error("내역서·일위대가에서 등록할 행 하나를 고르세요.");
const label = targets[which].label;
register(structuredClone(state.project), state.view, index, which);
if (!confirm(`"${row.Name ?? ""}" ${row.Size ?? ""} 를 ${label}에 등록하시겠습니까?`))
return true;
let code;
EstApp.change((project) => (code = register(project, state.view, index, which)));
state.notice = `${label}에 ${code}로 등록하고 이 행을 연결했습니다.`;
} else return false;
return true;
}
return { divide, register, handle };
})();