- 웹 계산·표시 옵션에 이어진 칸은 그 옵션에, 나머지는 값만, PC 설정은 브라우저에 - 바꾼 칸만 씀, 원본 적용 알림 - 코드표시 켜면 표에 코드 열, 레벨 표시 끄면 레벨 열 뺌 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
283 lines
15 KiB
JavaScript
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 };
|
|
})();
|