// 기성고 화면(native-extra.js 에서 옮김)과 원본 기성 「환경설정」(TfrmAccomOption — spec_pb1 §4: 주 메뉴 환경 설정 0x10ccddc 가
// 기성 문서일 때 · 기성 원가계산서 팝업 popupOptision 0xd479f4 Ctrl+U). 칸을 누르면 바로 옵션에 쓰고 「취소」면 연 때 값으로 되돌림(d45e1c).
// 공통적용(코드·수량 0·금액 0·비목순서·품명정렬·품명 두줄·Enter 이동·바탕색)은 다른 표와 같은 계산 환경 Display,
// 기성고(레벨·비고란 발췌근거·집계공종 진하게·사정금액·노재경 구분·실적 비목)는 AccomOption. 원본도 처리기가 없는
// 자동저장 분·가로세로 계산 확인·가로 계산방법·재료비 처리·이윤 사사오입은 값만 담는다(화면·계산 불변).
const EstProgressView = (() => {
const num = (v) => Number(v) || 0;
const COMMON = {
code: true,
gnZero: true,
costZero: true,
order: 0,
align: 0,
twoLine: false,
enter: 0,
color: "",
autoSave: false,
saveTime: 10,
};
const ACCOM = {
level: false,
gungu: false,
sumBold: false,
calcCheck: false,
horCalc: 0,
sajung: true,
gubun: false,
siljuk: false,
gcTo: 0,
eyunRound: false,
};
const get = (options) => ({
...COMMON,
...(options?.Display || {}),
...ACCOM,
...(options?.AccomOption || {}),
});
const ORDERS = ["TNGJ", "TGNJ", "NGJT", "GNJT"];
const PART = { T: "합계", N: "노무", G: "재료", J: "경비", S: "실적" };
// 비목 금액 열(금회): 비목순서대로, 실적은 실적 비목 표시 때만 경비 뒤에
function parts(o) {
const seq = [...(ORDERS[o.order] || ORDERS[0])];
if (o.siljuk) seq.splice(seq.indexOf("J") + 1, 0, "S");
return seq;
}
function view(state, project, { box, button, table }) {
const { esc } = EstApp,
o = get(project.options);
const shown = (v, zero) => (!zero && !num(v) ? "" : EstApp.number(v));
const qty = (v) => shown(v, o.gnZero),
cost = (v) => shown(v, o.costZero);
state.roundId ||= project.progress[0]?.id;
const round = project.progress.find((r) => r.id === state.roundId);
let body = '
회차를 추가하세요.
',
tools =
button("add-round", "기성 회차 추가") +
'' +
(project.progress.length ? button("clear-progress", "기성 전체 초기화") : "") +
button("accom-option", "환경 설정");
if (round) {
tools +=
`` +
button(
round.locked ? "unlock-round" : "lock-round",
round.locked ? "확정 해제(이후 포함)" : "회차 확정·저장",
) +
button("fill-progress", "전량·비율 일괄 입력") +
button("rename-round", "회차 이름") +
button("delete-round", "회차 삭제") +
button("reset-round-inputs", "회차 입력 초기화") +
`` +
``;
let output;
try {
output =
round.locked && round.result ? round.result : EstBusiness.progress(project, round.id);
} catch (e) {
output = { rows: [], errors: [e.message] };
}
state.business = output;
const page = EstBlockActions.progressPage(state, output.rows);
tools +=
button("progress-prev", "이전", state.progressPage === 0 ? "disabled" : "") +
`${state.progressPage + 1} / ${page.pages}쪽 · ${page.total}행` +
button("progress-next", "다음", state.progressPage + 1 >= page.pages ? "disabled" : "");
const seq = o.gubun ? parts(o) : ["T"];
const nameStyle = [
["", "text-align:right", "text-align:center"][o.align] || "",
o.twoLine ? "white-space:normal;max-width:16em" : "white-space:nowrap",
]
.filter(Boolean)
.join(";");
const headers = [
...(o.code ? ["코드"] : []),
...(o.level ? ["레벨"] : []),
"공종",
"도급수량",
"전회수량",
"금회 입력",
"입력방법",
...(o.sajung ? ["사정 입력", "입력방법"] : []),
...seq.map((p) => (p === "T" ? "금회금액" : `금회 ${PART[p]}`)),
...(o.sajung ? ["사정금액"] : []),
"누계수량",
"누계금액",
...(o.gungu ? ["비고"] : []),
];
body =
table(
headers,
page.rows.map(({ row, index }) => {
const editable = ![54, 55, 58, 59, 62, 67].includes(Number(row.LinkIndex));
const key = EstBusiness.rowKey(row, index),
input = round.inputs?.[key] || {};
const edit = (prefix) =>
`` +
(input[prefix + "Mode"] === "components"
? EstMath.components
.map(
(field, i) =>
``,
)
.join("")
: "");
const method = (prefix) =>
!editable
? "자동계산"
: ``;
const bold = o.sumBold && num(row.Level) > 0 ? ";font-weight:bold" : "";
return [
...(o.code ? [esc(row.Code)] : []),
...(o.level ? [num(row.Level) || ""] : []),
`${esc(row.Name)}`,
qty(row.GN),
qty(row.JunGN),
edit("Gum"),
method("Gum"),
...(o.sajung ? [edit("Sa"), method("Sa")] : []),
...seq.map((p) => cost(row[`Gum${p}C`])),
...(o.sajung ? [cost(row.SaTC)] : []),
qty(row.TotalGN),
cost(row.TotalTC),
...(o.gungu ? [esc(row.Bigo ?? "")] : []),
];
}),
) +
`${esc((output.errors || []).join(" / "))} 금회 도급액 ${EstApp.number(output.total)} · 기성비율 ${EstApp.number(output.ratio)}%
`;
body += EstProgressWonga.render(state, round, output);
}
const html = box(
"기성고",
tools,
body,
"전회는 직전 확정 회차 누계를 읽습니다. 금회·사정은 수량·금액·비율 중 하나를 입력해 계산합니다.",
);
return o.color ? `${html}
` : html;
}
// 환경설정 창: 칸을 바꾸면 바로 옵션에 쓰고(화면에 즉시), 「취소」 는 연 때 값으로 되돌림
let opened = null;
function dialog(state) {
const { esc } = EstApp,
o = get(state.project.options);
opened = {
Display: structuredClone(state.project.options.Display),
AccomOption: structuredClone(state.project.options.AccomOption),
};
const check = (name, label, extra = "") =>
``;
const pick = (name, label, choices, extra = "") =>
``;
EstApp.showDialog(
"환경설정",
`공통적용
${check("code", "코드표시")}${check("gnZero", '수량 "0" 표시')}${check("costZero", '금액 "0" 표시')}${check("autoSave", "데이타 자동저장")}${pick("order", "비목순서", ["합/노/재/경", "합/재/노/경", "노/재/경/합", "재/노/경/합"], o.gubun ? "" : "disabled")}${pick("align", "품명정렬", ["좌정렬", "우정렬", "중앙정렬"])}${pick("twoLine", "품명표기", ["한줄로 표시", "두줄로 표시"])}${pick("enter", "Enter 뒤 셀 이동", ["이동안함", "좌", "우", "위", "아래"])}
` +
`기성고
${check("level", "레벨 표시")}${check("gungu", "비고란에 발췌근거 표시")}${check("sumBold", "집계공종 진하게 표시")}${check("calcCheck", "가로,세로 계산 확인")}${pick("horCalc", "데이타(가로) 계산방법", ["절하", "절상"])}${check("sajung", "사정금액 표시")}${check("gubun", "노,재,경 구분 표시")}${check("siljuk", "실적 비목 표시", o.gubun ? "" : "disabled")}
` +
`원가계산
${pick("gcTo", "순직접공사비의 재료비 처리", ["간접재료비", "직접재료비"])}${check("eyunRound", "이윤 비율 사사오입")}
` +
` `,
);
}
function set(name, value) {
const options = EstApp.state.project.options,
bucket = name in COMMON ? "Display" : "AccomOption";
const flag = typeof COMMON[name] === "boolean" || typeof ACCOM[name] === "boolean";
const typed = flag
? typeof value === "string"
? value === "1"
: Boolean(value)
: name === "color"
? value
: num(value);
options[bucket] = { ...(options[bucket] || {}), [name]: typed };
const box = document.getElementById("dialog-content");
if (name === "autoSave") box.querySelector('[data-ao="saveTime"]').disabled = !typed;
if (name === "gubun") {
box.querySelector('[data-ao="order"]').disabled = !typed;
box.querySelector('[data-ao="siljuk"]').disabled = !typed;
}
EstApp.render();
}
function close(apply) {
const options = EstApp.state.project.options;
if (opened) {
const now = { Display: options.Display, AccomOption: options.AccomOption };
for (const k of ["Display", "AccomOption"])
if (opened[k] === undefined) delete options[k];
else options[k] = opened[k];
// 적용: 연 때 값으로 돌린 뒤 바뀐 값을 한 번에 기록(되돌리기 한 단계)
if (apply) EstApp.change((project) => Object.assign(project.options, now), false);
}
opened = null;
EstApp.byId("dialog").close();
EstApp.render();
}
// Enter 뒤 셀 이동(ComboBoxEnter 62c+0x60): 표 안 입력칸에서 Enter 면 좌·우·위·아래 칸으로
function move(input, direction) {
const cell = input.closest("td"),
tr = cell?.parentElement;
if (!tr) return null;
const col = [...tr.children].indexOf(cell);
let target = null;
if (direction === 1 || direction === 2) {
const cells = [...tr.children];
for (
let c = col + (direction === 1 ? -1 : 1);
c >= 0 && c < cells.length;
c += direction === 1 ? -1 : 1
)
if ((target = cells[c].querySelector("input:not([disabled]),select:not([disabled])")))
break;
} else if (direction === 3 || direction === 4) {
for (
let r = direction === 3 ? tr.previousElementSibling : tr.nextElementSibling;
r;
r = direction === 3 ? r.previousElementSibling : r.nextElementSibling
)
if (
(target = r.children[col]?.querySelector("input:not([disabled]),select:not([disabled])"))
)
break;
}
return target;
}
function install() {
document.addEventListener("change", (event) => {
const t = event.target;
if (t.dataset?.ao === undefined) return;
set(t.dataset.ao, t.type === "checkbox" ? t.checked : t.value);
});
document.addEventListener("keydown", (event) => {
const state = EstApp.state;
if (state?.view !== "progress") return;
if (event.ctrlKey && event.key.toLowerCase() === "u") {
event.preventDefault();
dialog(state);
return;
}
const t = event.target;
if (event.key !== "Enter" || !t.matches?.("table.sheet input, table.sheet select")) return;
const direction = num(get(state.project.options).enter);
if (!direction) return;
const target = move(t, direction);
if (target) {
event.preventDefault();
t.dispatchEvent(new Event("change", { bubbles: true }));
setTimeout(() =>
(
document.querySelector(
`[data-key="${CSS.escape(target.dataset.key || "")}"][data-progress="${target.dataset.progress}"]`,
) || target
).focus(),
);
}
});
}
if (typeof document !== "undefined" && document.addEventListener) install();
function handle(name, state) {
if (name === "accom-option") dialog(state);
else if (name === "accom-option-ok") close(true);
else if (name === "accom-option-cancel") close(false);
else return false;
return true;
}
return { view, get, parts, move, handle, dialog };
})();