// 기성고 화면(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 }; })();