Files
Aislo/tools/est-test/native-option-all.js
T
eomsangdonandClaude Opus 5.5 04a432c08a feat(est-test): 환경설정 전체 창(탭 5·92칸, 적용 알림·순공사비 명칭)
- 웹 계산·표시 옵션에 이어진 칸은 그 옵션에, 나머지는 값만, PC 설정은 브라우저에
- 바꾼 칸만 씀, 원본 적용 알림
- 코드표시 켜면 표에 코드 열, 레벨 표시 끄면 레벨 열 뺌

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

283 lines
15 KiB
JavaScript

// 원본 환경설정 TfrmOption 「적 용」(btnLoad 0xd034f8 → FormClose 후처리 d03d20 — spec_pb2 §9): 탭별 칸 전부.
// 웹 계산·표시에 이어진 칸은 그 옵션(HeadDigits·CostValueTrunc·MidCostDiget·DollarCostCut·GCToWonga·EYunUp·
// GangubExceptJodalRatio·VerFormat·AutoCalc·OldRecalcMethod·JungSangShow·Display·AccomOption)에 쓰고, 나머지는 계산 환경 Env 에
// 값만 담는다. PC 설정(INI: 자동저장 분·백업·새이름 공사명·불러오기창·찾을문자크기·금액 변경확인·자재 틀고정)은 이 브라우저에.
// 적용 때 원본 알림: 자동계산 끔/켬 · 표준단가비목 · 조달청 계산방식 · 산식 표시 · 공종+품명 합치기 · 순공사비 명칭(행 이름 바꿈).
const EstOptionAll = (() => {
const num = (v) => Number(v) || 0;
const PC = "est-pc-options";
const yes = (k, label, at, def = false) => ({ k, label, kind: "check", at, def });
const pick = (k, label, choices, at, def = 0) => ({ k, label, kind: "pick", choices, at, def });
const int = (k, label, min, max, at, def = 0) => ({ k, label, kind: "int", min, max, at, def });
const text = (k, label, at, def = "") => ({ k, label, kind: "text", at, def });
const ORDER = [
"합/노/재/경",
"합/재/노/경",
"노/재/경/합",
"재/노/경/합",
"합/경/재/노",
"경/재/노/합",
];
const GUNGU = [
"일위대가",
"단가산출",
"기계경비",
"표준단가",
"견적단가",
"자재단가",
"노임단가",
];
// at: "opt:키"(계산 옵션) · "disp:키"(Display) · "accom:키"(AccomOption) · "pc"(이 브라우저) · 없음 = Env
const TABS = [
[
"공통적용",
[
pick("order", "비목순서", ORDER, "disp:order"),
pick("align", "품명정렬", ["좌정렬", "우정렬", "중앙정렬"], "disp:align"),
pick("twoLine", "품명표기", ["한줄로 표시", "두줄로 표시"], "disp:twoLine"),
pick("enter", "Enter 후 셀 이동", ["이동안함", "좌", "우", "위", "아래"], "disp:enter"),
yes("code", "코드표시", "disp:code"),
yes("gnZero", '수량 "0" 표시', "disp:gnZero", true),
yes("costZero", '금액 "0" 표시', "disp:costZero", true),
int("gnDigits", "수량 표시 자리수", 0, 6, null, 3),
int("nachalDigits", "낙찰율 표시 자리수", 0, 6, null, 2),
yes("nachalShow", "낙찰율 표시"),
yes("haljungShow", "할증율 표시"),
yes("siljukBimog", "표준단가비목표시"),
yes("autoSave", "데이타 자동저장", "disp:autoSave"),
int("saveTime", "자동저장 분", 5, 30, "pc", 10),
yes("siljukSearch", "표준단가 동시검색"),
yes("numSort", "공종일련번호 자동정렬"),
yes("jodalMethod", "조달청 계산방식적용"),
yes("nameMatch", "명칭 불일치 검색"),
yes("valueCompare", "신,구 단가비교 보기"),
int("searchLength", "찾을문자크기", 1, 20, "pc", 2),
text("color", "바탕색(#rrggbb)", "disp:color"),
yes("backup", "백업 저장", "pc"),
yes("projectAutoChange", "새이름 저장 시 공사명 변경", "pc"),
yes("searchFormAuto", "불러오기창 위치 기억", "pc"),
],
],
[
"내역서",
[
pick("nameShow", "공종 표시", ["공종=(공종+품명)", "별도"], null, 1),
yes("gungu", "비고란에 발췌근거 표시", "accom:gungu"),
yes("level", "레벨 표시", "accom:level"),
yes("projectTop", "공사명 설계서 첫줄에 표시"),
yes("titleTopPrint", "공사명 총괄표 첫줄에 인쇄"),
yes("jabbi", "내역서에 제잡비 표시"),
yes("sumBold", "집계공종 진하게", "accom:sumBold"),
yes("nachalGubun", "낙찰율 노,재,경 구분 표시"),
pick("totalPos", "순공사비 계산 위치", ["하단", "상단"]),
text("totalName", "순공사비 명칭", null, "순공사비"),
yes("firstTotal", "새공사 시작시 순공사비 자동생성", null, true),
pick("horCalc", "데이타(가로) 계산방법", ["절하", "절상"], "accom:horCalc"),
yes("calcCheck", "가로,세로 계산 확인", "accom:calcCheck"),
yes("calcAuto", "내역서 자동계산", "opt:AutoCalc", true),
yes("costCompareMsg", "재계산후 공사금액 변경확인", "pc"),
...GUNGU.map((g, i) => yes(`gungu${i}`, `발췌근거 표시 — ${g}`)),
],
],
[
"일위·단산·기계",
[
pick("headDigits", "일위대가 금액 계산 자리수", ["원단위", "소수1위"], "opt:HeadDigits"),
pick("headHorCalc", "일위대가 데이타(가로) 계산방법", ["절하", "절상"]),
yes("headBigo", "일위대가 비고란 발췌근거"),
yes("headInfo", "일위대가 공종 정보창"),
pick("costMethod", "단가산출 금액란", ["소수1위", "절하"], "opt:CostValueTrunc"),
int("midCost", "단가산출 중간계산 결과값 자리수", 0, 6, "opt:MidCostDiget", 2),
yes("sansic", "수식을 산식으로 변환 표시"),
yes("susicCheck", "수식오류 검색"),
yes("linkValueEdit", "연결 항목 편집허용"),
yes("costShowToHead", "공종 라인 총단가"),
yes("costInfo", "단가산출 정보창"),
yes("dollarCut", "외화장비 손료 1,000단위 미만 절하", "opt:DollarCostCut"),
yes("machInfo", "기계경비 정보창"),
],
],
[
"표준·자재·원가",
[
pick("remainBimog", "노무비율계산후 잔여단가 적용비목", ["재료비", "경비"]),
yes("siljukCode", "표준단가 비고란 코드"),
yes("stuffcGungu", "자재 비고란 발췌근거"),
yes("stuffcFixed", "자재 품명,규격 틀고정", "pc"),
pick("gcTo", "순직접공사비의 재료비", ["간접재료비", "직접재료비"], "opt:GCToWonga"),
yes("eyun", "이윤 비율 사사오입", "opt:EYunUp"),
yes("gangub", "안전관리비 관급자재대 조달수수료 제외", "opt:GangubExceptJodalRatio"),
yes("digetOldCost", "자리수 조정후 수식에 원금액 표시"),
yes("jungsan", "정산금액 표시", "opt:JungSangShow"),
pick("format", "원가계산서 양식 방향", ["세로", "가로"], "opt:VerFormat"),
yes("selectPrint", "원가 선택항목만 인쇄"),
],
],
[
"설계변경·수량·심사",
[
pick("changeOrder", "상단·하단", ["상단=당초 · 하단=변경", "상단=변경 · 하단=당초"]),
text("upText", "상단 표시문자", null, "당초"),
text("downText", "하단 표시문자", null, "변경"),
yes("upBold", "상단 굵게 인쇄"),
yes("downBold", "하단 굵게 인쇄"),
pick("nameShowMethod", "공종,품명,규격", ["변경만", "당초,변경 모두", "다르면 둘다"]),
pick("gunguShowMethod", "근거자료", ["신규만", "전부"]),
yes("oldCostShow", "신규공종 당초단가 표시"),
yes("nachalAuto", "신규공종 낙찰율 자동계산"),
yes("nachalToBigo", "비고란에 낙찰율 표시"),
pick("recalc", "재계산", ["신규만", "변경단가만", "전체"], "opt:OldRecalcMethod"),
yes("changeZero", "변경금액만 0 표시"),
yes("cellMerge", "증감 셀 병합"),
yes("mnsShow", "감 부호(-) 표시"),
yes("changeBigo", "변경 비고란 표시"),
yes("changeCalcAuto", "변경 내역서 자동계산", null, true),
pick("mulCalc", "수량산출 계산결과값", ["사사오입", "절하", "절상"]),
int("mulDigits", "수량산출 소수이하 자리수", 0, 6, null, 3),
yes("mulSansic", "수량산출 산식 변환"),
yes("mulColor", "도면칼라 인쇄"),
pick("investPrint", "계약심사 인쇄방법", ["내역서 양식", "설계변경 양식"]),
pick("investItems", "계약심사 인쇄항목", ["전부", "변경만"]),
],
],
];
const ALL = TABS.flatMap(([, items]) => items);
const pc = () => JSON.parse(localStorage.getItem(PC) || "{}");
// 지금 값 읽기
function read(options, item) {
const [where, key] = String(item.at || "").split(":");
let v;
if (where === "opt") v = options[key];
else if (where === "disp") v = options.Display?.[key];
else if (where === "accom") v = options.AccomOption?.[key];
else if (where === "pc") v = typeof localStorage !== "undefined" ? pc()[item.k] : undefined;
else v = options.Env?.[item.k];
if (item.k === "format") return v === false ? 1 : 0; // VerFormat true = 세로
if (item.k === "headDigits") return num(v) > 0 ? 1 : 0;
if (item.k === "twoLine") return v ? 1 : 0;
return v ?? item.def;
}
// 고친 값 → 옵션 쓰기(project.options 를 직접 고침). pcOut 은 이 브라우저 값
function write(options, values, pcOut = {}) {
for (const item of ALL) {
if (!(item.k in values)) continue;
let v = values[item.k];
// 바꾼 칸만 씀(웹 고유 값 — 예: 일위 자리수 2 — 을 그대로 두게)
const now = read(options, item);
if (
item.kind === "check"
? Boolean(now) === Boolean(v)
: item.kind === "text"
? String(now ?? "") === String(v)
: num(now) === num(v)
)
continue;
if (item.kind === "check") v = Boolean(v);
else if (item.kind !== "text") v = num(v);
if (item.k === "format") v = v === 0;
if (item.k === "twoLine") v = v === 1;
if (["costMethod", "gcTo"].includes(item.k)) v = v === 1;
const [where, key] = String(item.at || "").split(":");
if (where === "opt") options[key] = v;
else if (where === "disp") options.Display = { ...(options.Display || {}), [key]: v };
else if (where === "accom")
options.AccomOption = { ...(options.AccomOption || {}), [key]: v };
else if (where === "pc") pcOut[item.k] = v;
else options.Env = { ...(options.Env || {}), [item.k]: v };
}
return pcOut;
}
let tab = 0;
function dialog(state) {
const { esc } = EstApp,
o = state.project.options;
const field = (item) => {
const v = read(o, item);
if (item.kind === "check")
return `<label><input type="checkbox" data-oa="${item.k}" ${v ? "checked" : ""}> ${esc(item.label)}</label>`;
if (item.kind === "pick")
return `<label>${esc(item.label)} <select data-oa="${item.k}">${item.choices.map((c, i) => `<option value="${i}" ${num(v) === i ? "selected" : ""}>${esc(c)}</option>`).join("")}</select></label>`;
if (item.kind === "int")
return `<label>${esc(item.label)} <input type="number" data-oa="${item.k}" min="${item.min}" max="${item.max}" value="${num(v)}" style="width:60px"></label>`;
return `<label>${esc(item.label)} <input data-oa="${item.k}" value="${esc(v ?? "")}"></label>`;
};
EstApp.showDialog(
"환경설정",
`<div class="business-tools">${TABS.map(([name], i) => `<button data-action="option-all-tab" data-tab="${i}" ${i === tab ? 'aria-pressed="true"' : ""}>${esc(name)}</button>`).join("")}</div>${TABS.map(([, items], i) => `<div class="form-fields" data-oa-tab="${i}" ${i === tab ? "" : "hidden"}>${items.map(field).join("")}</div>`).join("")}<p><button data-action="option-all-apply">적 용</button> <button data-action="option-all-cancel">취 소</button> <button data-action="option-all-web">웹 계산 환경(도급액 고정 등)</button> <span id="option-all-error" role="alert"></span></p>`,
);
}
function collect() {
const values = {};
for (const node of document.querySelectorAll("#dialog-content [data-oa]"))
values[node.dataset.oa] = node.type === "checkbox" ? node.checked : node.value;
for (const item of ALL)
if (item.kind === "int") {
const v = Number(values[item.k]);
if (!Number.isInteger(v) || v < item.min || v > item.max)
throw Error(`${item.label}은(는) ${item.min}~${item.max} 정수입니다.`);
}
if (values.color && !/^#[0-9a-f]{6}$/i.test(values.color))
throw Error("바탕색은 #rrggbb 꼴입니다.");
return values;
}
// 적용(d03d20 후처리): 알림과 자료 반영
function apply(state) {
const values = collect(),
before = state.project.options,
notes = [];
const was = (k) =>
read(
before,
ALL.find((i) => i.k === k),
);
if (was("calcAuto") && !values.calcAuto && !confirm("내역서의 모든 계산기능이 해제됩니다."))
values.calcAuto = true;
if (!was("calcAuto") && values.calcAuto) notes.push("내역서를 재계산 시켜주세요.");
if (Boolean(was("siljukBimog")) !== values.siljukBimog)
notes.push("반드시 재계산을 시켜야 금액이 정상적으로 표시됩니다.");
if (Boolean(was("jodalMethod")) !== values.jodalMethod) notes.push("전체 재계산을 해주세요.");
if (Boolean(was("sansic")) !== values.sansic) notes.push("일건서류 전체를 재계산 합니다.");
if (
num(was("nameShow")) === 1 &&
num(values.nameShow) === 0 &&
!confirm("공종과 품명을 합치면 되돌릴수 없습니다.")
)
values.nameShow = 1;
const rename = String(values.totalName ?? "").trim() && values.totalName !== was("totalName");
let pcOut;
EstApp.change((project) => {
pcOut = write(project.options, values);
if (rename)
for (const row of project.tables.EstMainDat)
if (num(row.LinkIndex) === 54) row.Name = values.totalName.trim();
});
localStorage.setItem(PC, JSON.stringify({ ...pc(), ...pcOut }));
EstApp.byId("dialog").close();
state.notice = notes.join(" ") || "환경설정을 적용했습니다.";
EstApp.render();
}
function handle(name, state, node) {
if (name === "option-all") dialog(state);
else if (name === "option-all-tab") {
tab = Number(node.dataset.tab);
for (const box of document.querySelectorAll("#dialog-content [data-oa-tab]"))
box.hidden = Number(box.dataset.oaTab) !== tab;
for (const b of document.querySelectorAll('[data-action="option-all-tab"]'))
b.setAttribute("aria-pressed", String(Number(b.dataset.tab) === tab));
} else if (name === "option-all-apply")
try {
apply(state);
} catch (error) {
EstApp.byId("option-all-error").textContent = error.message;
}
else if (name === "option-all-cancel") EstApp.byId("dialog").close();
else if (name === "option-all-web") EstSettings.show();
else return false;
return true;
}
return { TABS, read, write, handle, dialog };
})();