699 lines
30 KiB
JavaScript
699 lines
30 KiB
JavaScript
const EstExtra = (() => {
|
|
function field(section, index, key, value, type = "number") {
|
|
return `<input data-extra="${section}" data-row="${index}" data-field="${key}" type="${type}" step="any" value="${EstApp.esc(value)}" aria-label="${EstApp.esc(key)}">`;
|
|
}
|
|
function button(action, text, extra = "") {
|
|
return `<button data-action="${action}" ${extra}>${text}</button>`;
|
|
}
|
|
function table(headers, rows) {
|
|
return `<table class="sheet"><thead><tr>${headers.map((h) => `<th>${h}</th>`).join("")}</tr></thead><tbody>${rows.map((row) => `<tr>${row.map((cell) => `<td>${cell}</td>`).join("")}</tr>`).join("")}</tbody></table>`;
|
|
}
|
|
function box(title, tools, body, note = "") {
|
|
return `<div class="business-tools"><b>${title}</b>${tools}</div>${body}${note ? `<p class="business-note">${note}</p>` : ""}`;
|
|
}
|
|
function render(state) {
|
|
if (state.view === "review") {
|
|
EstReview.render(state);
|
|
return;
|
|
}
|
|
if (state.view === "schedule") {
|
|
EstSchedule.render();
|
|
return;
|
|
}
|
|
if (state.view === "issues") {
|
|
EstIssues.render();
|
|
return;
|
|
}
|
|
if (state.view === "attachments") {
|
|
EstAttachments.render();
|
|
return;
|
|
}
|
|
if (["standard", "standard-change"].includes(state.view)) {
|
|
EstStandard.render();
|
|
return;
|
|
}
|
|
if (["estimate-usage", "estimate-change"].includes(state.view)) {
|
|
EstEstimate.render();
|
|
return;
|
|
}
|
|
if (["material-usage", "material-change", "labor-usage", "labor-change"].includes(state.view)) {
|
|
EstBasicPrices.render();
|
|
return;
|
|
}
|
|
if (["machine-usage", "machine-change"].includes(state.view)) {
|
|
EstMachine.render();
|
|
return;
|
|
}
|
|
const { project, view } = state,
|
|
{ esc, number, byId } = EstApp;
|
|
let html = "";
|
|
if (view === "quantity") {
|
|
if (project.quantitySheets?.length && state.quantitySheet !== "manual") {
|
|
EstQuantity.render(state);
|
|
return;
|
|
}
|
|
const rows = EstQuantity.manual(project.quantities).rows.map((row, index) => {
|
|
return [
|
|
field("quantities", index, "Code", row.Code, "text"),
|
|
field("quantities", index, "Name", row.Name, "text"),
|
|
field("quantities", index, "TargetCode", row.TargetCode, "text") +
|
|
EstQuantity.targetSelect(project, row, "manual", index),
|
|
field("quantities", index, "formula", row.formula, "text"),
|
|
field("quantities", index, "digits", row.digits ?? 2),
|
|
esc(row.Error || number(row.GN)),
|
|
button(
|
|
"move-quantity",
|
|
"↑",
|
|
`data-row="${index}" data-direction="-1" ${index === 0 ? "disabled" : ""}`,
|
|
) +
|
|
button(
|
|
"move-quantity",
|
|
"↓",
|
|
`data-row="${index}" data-direction="1" ${index === project.quantities.length - 1 ? "disabled" : ""}`,
|
|
) +
|
|
button("delete-extra", "삭제", `data-section="quantities" data-row="${index}"`),
|
|
];
|
|
});
|
|
html =
|
|
EstQuantity.selector(state) +
|
|
box(
|
|
"수량산출서",
|
|
button("add-quantity", "산출 항목 추가") + button("apply-quantity", "내역에 수량 반영"),
|
|
table(["산출코드", "품명", "연결 내역코드", "수식", "소수자리", "결과", ""], rows),
|
|
"앞선 산출 항목을 [산출코드]로 참조합니다. 내역 반영은 연결된 코드의 산출값을 합산하여 수량에 반영합니다.",
|
|
);
|
|
} else if (view === "change") {
|
|
const changes = EstBusiness.changes(project);
|
|
const page = EstBlockActions.changePage(state, changes);
|
|
html = box(
|
|
"설계변경",
|
|
button("capture-baseline", "현재 내역을 당초로 확정") +
|
|
button("load-contract", "당초·계약 내역 불러오기") +
|
|
button("recalculate-original", "당초 단가 재계산") +
|
|
button("bid-rates", "낙찰률 적용") +
|
|
button("restore-bid", "당초단가(100%) 복원") +
|
|
button("change-prev", "이전", state.changePage === 0 ? "disabled" : "") +
|
|
`<span>${state.changePage + 1} / ${page.pages}쪽 · ${page.total}행</span>` +
|
|
button("change-next", "다음", state.changePage + 1 >= page.pages ? "disabled" : ""),
|
|
table(
|
|
["구분", "코드", "품명", "당초 수량", "변경 수량", "당초 금액", "변경 금액", "증감"],
|
|
page.rows.map(({ row }) => [
|
|
esc({ new: "신규", deleted: "삭제", changed: "변경", unchanged: "동일" }[row.status]),
|
|
esc(row.Code),
|
|
esc(row.Name),
|
|
number(row.originalGN),
|
|
number(row.GN),
|
|
number(row.originalTC),
|
|
number(row.TC),
|
|
number(row.difference),
|
|
]),
|
|
) +
|
|
table(
|
|
["원가 구분", "비목", "당초 금액", "변경 금액", "증감"],
|
|
EstBusiness.costChanges(project).map((row) => [
|
|
esc(row.Gubun),
|
|
esc(row.Name),
|
|
number(row.originalCost),
|
|
number(row.changedCost),
|
|
number(row.difference),
|
|
]),
|
|
),
|
|
project.baseline
|
|
? `당초: ${esc(project.baseline.name)}. 당초 자료는 별도 사본으로 보존합니다.`
|
|
: changes.some((row) => row.originalSource === "file")
|
|
? "원본 파일에 저장된 당초 수량·금액과 비교합니다."
|
|
: "현재 작업을 저장한 뒤 당초 내역을 확정하세요.",
|
|
);
|
|
} else if (view === "progress") {
|
|
state.roundId ||= project.progress[0]?.id;
|
|
const round = project.progress.find((r) => r.id === state.roundId);
|
|
let body = '<p class="empty">회차를 추가하세요.</p>',
|
|
tools =
|
|
button("add-round", "기성 회차 추가") +
|
|
(project.progress.length ? button("clear-progress", "기성 전체 초기화") : "");
|
|
if (round) {
|
|
tools +=
|
|
`<select id="round-choice">${project.progress.map((r) => `<option value="${esc(r.id)}" ${r.id === round.id ? "selected" : ""}>${esc(r.name)}${r.locked ? " (확정)" : ""}</option>`).join("")}</select>` +
|
|
button(
|
|
round.locked ? "unlock-round" : "lock-round",
|
|
round.locked ? "확정 해제(이후 포함)" : "회차 확정·저장",
|
|
) +
|
|
button("fill-progress", "전량·비율 일괄 입력") +
|
|
button("rename-round", "회차 이름") +
|
|
button("delete-round", "회차 삭제") +
|
|
button("reset-round-inputs", "회차 입력 초기화") +
|
|
`<label><input id="progress-components" type="checkbox" ${round.separateComponents !== false ? "checked" : ""} ${round.locked ? "disabled" : ""}> 비목별 계산</label><label>금액 소수자리 <input id="progress-digits" type="number" min="0" max="4" value="${round.amountDigits ?? 0}" ${round.locked ? "disabled" : ""}></label>` +
|
|
`<label><input id="use-approved" type="checkbox" ${round.approved ? "checked" : ""} ${round.locked ? "disabled" : ""}> 누계에 사정 사용</label>`;
|
|
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" : "") +
|
|
`<span>${state.progressPage + 1} / ${page.pages}쪽 · ${page.total}행</span>` +
|
|
button("progress-next", "다음", state.progressPage + 1 >= page.pages ? "disabled" : "");
|
|
body =
|
|
table(
|
|
[
|
|
"코드",
|
|
"공종",
|
|
"도급수량",
|
|
"전회수량",
|
|
"금회 입력",
|
|
"입력방법",
|
|
"사정 입력",
|
|
"입력방법",
|
|
"금회금액",
|
|
"사정금액",
|
|
"누계수량",
|
|
"누계금액",
|
|
],
|
|
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 data-progress="${prefix}" data-key="${esc(key)}" type="number" step="any" value="${input[prefix] ?? row[prefix + "GN"] ?? 0}" ${round.locked || !editable || input[prefix + "Mode"] === "components" ? "disabled" : ""}>` +
|
|
(input[prefix + "Mode"] === "components"
|
|
? EstMath.components
|
|
.map(
|
|
(field, i) =>
|
|
`<label>${["노무", "재료", "경비", "실적"][i]}<input data-progress="${prefix + field}" data-key="${esc(key)}" type="number" step="any" value="${input[prefix + field] ?? 0}" ${round.locked ? "disabled" : ""}></label>`,
|
|
)
|
|
.join("")
|
|
: "");
|
|
const method = (prefix) =>
|
|
!editable
|
|
? "자동계산"
|
|
: `<select data-progress="${prefix}Mode" data-key="${esc(key)}" ${round.locked ? "disabled" : ""}>${[
|
|
["quantity", "수량"],
|
|
["amount", "금액"],
|
|
["ratio", "비율 %"],
|
|
["components", "비목 금액"],
|
|
]
|
|
.map(
|
|
([v, label]) =>
|
|
`<option value="${v}" ${v === (input[`${prefix}Mode`] || "quantity") ? "selected" : ""}>${label}</option>`,
|
|
)
|
|
.join("")}</select>`;
|
|
return [
|
|
esc(row.Code),
|
|
esc(row.Name),
|
|
number(row.GN),
|
|
number(row.JunGN),
|
|
edit("Gum"),
|
|
method("Gum"),
|
|
edit("Sa"),
|
|
method("Sa"),
|
|
number(row.GumTC),
|
|
number(row.SaTC),
|
|
number(row.TotalGN),
|
|
number(row.TotalTC),
|
|
];
|
|
}),
|
|
) +
|
|
`<p class="business-note">${esc((output.errors || []).join(" / "))} 금회 도급액 ${number(output.total)} · 기성비율 ${number(output.ratio)}%</p>`;
|
|
body += table(
|
|
["원가 비목", "도급금액", "전회", "금회", "사정", "누계"],
|
|
(output.costs || []).map((row) => [
|
|
esc(row.Name || row.Gubun),
|
|
number(row.ContractCost),
|
|
number(row.JunCost),
|
|
number(row.GumCost),
|
|
number(row.SaCost),
|
|
number(row.TotalCost),
|
|
]),
|
|
);
|
|
}
|
|
html = box(
|
|
"기성고",
|
|
tools,
|
|
body,
|
|
"전회는 직전 확정 회차 누계를 읽습니다. 금회·사정은 수량·금액·비율 중 하나를 입력해 계산합니다.",
|
|
);
|
|
} else if (view === "escalation") {
|
|
const output = EstBusiness.escalation(project);
|
|
state.business = output;
|
|
const comp = state.escalationComponent || "NU";
|
|
const choices = EstMath.units
|
|
.map(
|
|
(key, i) =>
|
|
`<option value="${key}" ${key === comp ? "selected" : ""}>${["노무", "재료", "경비", "실적"][i]}</option>`,
|
|
)
|
|
.join("");
|
|
const page = EstBlockActions.escalationPage(state, project.tables.EstMainDat);
|
|
const rows = page.rows.map(({ row, index }) => {
|
|
const result = output.rows[index];
|
|
return [
|
|
esc(row.Code),
|
|
esc(row.Name),
|
|
EstViews.cell("EstMainDat", index, `A${comp}`, row[`A${comp}`]),
|
|
number(result[`Contract${comp}`]),
|
|
EstViews.cell("EstMainDat", index, comp, row[comp]),
|
|
number(result[`D${comp}`]),
|
|
number(result[`E${comp}`]),
|
|
number(result[`F${comp}`]),
|
|
EstViews.cell("EstMainDat", index, "EscPlanned", row.EscPlanned || 0),
|
|
EstViews.cell("EstMainDat", index, "EscActual", row.EscActual || 0),
|
|
EstViews.cell("EstMainDat", index, "EscProgress", row.EscProgress || 0),
|
|
number(result.EscExcluded),
|
|
number(result.EscApplied),
|
|
`<input type="checkbox" data-esc-auto="${index}" ${row.EscAuto !== false ? "checked" : ""} aria-label="적용제외 자동계산">`,
|
|
EstViews.cell("EstMainDat", index, "EscManualQuantity", row.EscManualQuantity ?? 0),
|
|
EstViews.cell("EstMainDat", index, "EscConstruction", row.EscConstruction ?? 0),
|
|
EstViews.cell("EstMainDat", index, "EscPS", row.EscPS ?? 0),
|
|
EstViews.cell("EstMainDat", index, "EscBefore", row.EscBefore ?? 0),
|
|
];
|
|
});
|
|
html = box(
|
|
"물가변동 품목조정",
|
|
`<select id="escalation-component">${choices}</select>` +
|
|
button("capture-bid", "현재 단가를 입찰단가로 기록") +
|
|
button("load-bid", "입찰단가 불러오기") +
|
|
button("load-contract", "계약 내역 불러오기") +
|
|
button("load-progress-exclusion", "기성 누계수량 불러오기") +
|
|
button("reset-escalation", "물가변동 입력 초기화") +
|
|
button("capture-baseline", "계약 내역 확정") +
|
|
button("escalation-prev", "이전", state.escalationPage === 0 ? "disabled" : "") +
|
|
`<span>${state.escalationPage + 1} / ${page.pages}쪽 · ${page.total}행</span>` +
|
|
button(
|
|
"escalation-next",
|
|
"다음",
|
|
state.escalationPage + 1 >= page.pages ? "disabled" : "",
|
|
) +
|
|
`<label>조사 도급액(0=단가로 계산) <input type="number" step="any" data-option="EscDesignTotal" value="${project.options.EscDesignTotal ?? 0}"></label>`,
|
|
table(
|
|
[
|
|
"코드",
|
|
"품명",
|
|
"입찰 당시",
|
|
"계약 단가",
|
|
"변동 당시",
|
|
"등락률 %",
|
|
"등락폭",
|
|
"조정단가",
|
|
"예정공정수량",
|
|
"실행공정수량",
|
|
"기성수량",
|
|
"적용제외",
|
|
"적용수량",
|
|
"제외 자동",
|
|
"수동 제외수량",
|
|
"시공중 수량(참고)",
|
|
"PS 수량(참고)",
|
|
"선시공 수량(참고)",
|
|
],
|
|
rows,
|
|
),
|
|
`조사 도급액 ${number(output.designTotal)} · 계약 도급액 ${number(output.contractTotal)} · 조정 도급액 ${number(output.adjustedTotal)} · 적용제외(간접비 포함) ${number(output.excludedTotal)} · 적용대가 ${number(output.applied)} · 조정률 ${number(output.rate)}% · 조정금액 ${number(output.adjustment)}. 계약·입찰 파일은 동일 행 순서로 불러옵니다. ${esc(output.errors.join(" / "))}`,
|
|
);
|
|
} else if (view === "annual") {
|
|
const output = EstBusiness.annual(project, false);
|
|
state.business = output;
|
|
html = box(
|
|
"연차공사",
|
|
button("add-annual", "차수 추가") + button("reset-annual", "연차 차수 초기화(웹보조)"),
|
|
table(
|
|
["차수", "수량·금액 배분 수식", "순공사비", "도급액", "작업", ""],
|
|
project.annual.map((year, index) => [
|
|
field("annual", index, "name", year.name, "text"),
|
|
field("annual", index, "formula", year.formula, "text"),
|
|
number(output.results[index]?.project.calculation.direct.TC),
|
|
number(output.results[index]?.project.calculation.total),
|
|
button("open-annual", "차수 내역 열기", `data-row="${index}"`),
|
|
button(
|
|
"move-annual",
|
|
"↑",
|
|
`data-row="${index}" data-direction="-1" ${index === 0 ? "disabled" : ""}`,
|
|
) +
|
|
button(
|
|
"move-annual",
|
|
"↓",
|
|
`data-row="${index}" data-direction="1" ${index === project.annual.length - 1 ? "disabled" : ""}`,
|
|
) +
|
|
button("delete-extra", "삭제", `data-section="annual" data-row="${index}"`),
|
|
]),
|
|
),
|
|
`수식 예: [기준]*0.5, [기준]-[1차]. 같은 수식을 수량과 각 성분 금액에 따로 적용하고 금액은 원 미만 절사합니다. ${esc(output.errors.join(" / "))}`,
|
|
);
|
|
} else if (view === "resources") {
|
|
state.resourceKind = state.resourceKind || "all";
|
|
state.resourceByWork = state.resourceByWork ?? false;
|
|
const byWork = Boolean(state.resourceByWork);
|
|
const output = EstResources.calculate(project, byWork);
|
|
const filtered = output.rows.filter(
|
|
(r) => state.resourceKind === "all" || String(r.kind) === String(state.resourceKind),
|
|
);
|
|
const kinds = [
|
|
["all", "전체"],
|
|
["6", "노임"],
|
|
["5", "자재"],
|
|
["105", "기계"],
|
|
["21", "표준"],
|
|
["101", "견적"],
|
|
];
|
|
const kindSelect = `<label>구분 <select id="resource-kind-filter">${kinds
|
|
.map(
|
|
([k, label]) =>
|
|
`<option value="${k}" ${state.resourceKind === k ? "selected" : ""}>${esc(label)}</option>`,
|
|
)
|
|
.join("")}</select></label>`;
|
|
const byWorkCheck = `<label><input type="checkbox" id="resource-by-work" ${byWork ? "checked" : ""}> 공종별 전개</label>`;
|
|
const headers = byWork
|
|
? [
|
|
"공종코드",
|
|
"공종명",
|
|
"종류",
|
|
"코드",
|
|
"품명",
|
|
"규격",
|
|
"단위",
|
|
"소요량 / 환산시간",
|
|
"금액",
|
|
]
|
|
: ["종류", "코드", "품명", "규격", "단위", "소요량 / 환산시간", "금액"];
|
|
const rows = filtered.map((row) => {
|
|
const baseCells = [
|
|
esc({ 5: "자재", 6: "노임", 21: "표준", 101: "견적", 105: "기계" }[row.kind]),
|
|
esc(row.Code),
|
|
esc(row.Name),
|
|
esc(row.Size),
|
|
esc(row.Unite),
|
|
number(row.quantity),
|
|
number(row.amount),
|
|
];
|
|
return byWork ? [esc(row.WorkCode), esc(row.WorkName), ...baseCells] : baseCells;
|
|
});
|
|
html = box(
|
|
"동원인원·자재·장비",
|
|
`${kindSelect} ${byWorkCheck} <span>(총 ${filtered.length}건)</span>`,
|
|
table(headers, rows),
|
|
esc(output.errors.join(" / ")),
|
|
);
|
|
} else if (view === "drawing") {
|
|
const rows = project.tables.CadMainDat || [];
|
|
html = box(
|
|
"원본 도면·문서 자료",
|
|
"",
|
|
table(
|
|
["번호", "코드", "이름", "보존 필드"],
|
|
rows.map((row, index) => [
|
|
index + 1,
|
|
esc(row.Code),
|
|
esc(row.Name),
|
|
esc(
|
|
Object.keys(row)
|
|
.filter((key) => row[key]?.binary)
|
|
.join(", "),
|
|
),
|
|
]),
|
|
),
|
|
"가져온 도면 바이너리는 프로젝트에 보존됩니다. VEC0650/CAD 편집·출력의 재현 근거는 현재 자료에 없어 동작을 구현하지 않았습니다.",
|
|
);
|
|
}
|
|
byId("work-content").innerHTML = html;
|
|
}
|
|
|
|
function findDataUsages(project, target) {
|
|
if (!project || !target) return [];
|
|
const code = target.Code ? String(target.Code).trim() : "";
|
|
const name = target.Name ? EstNative.normalize(target.Name) : "";
|
|
const size = target.Size ? EstNative.normalize(target.Size) : "";
|
|
if (!code && !name) return [];
|
|
|
|
const results = [];
|
|
const tables = [
|
|
{ id: "EstMainDat", label: "내역서" },
|
|
{ id: "HeadMainDat", label: "일위대가" },
|
|
{ id: "CostMainDat", label: "단가산출" },
|
|
{ id: "MachMainDat", label: "기계경비" },
|
|
];
|
|
|
|
for (const info of tables) {
|
|
const rows = project.tables[info.id] || [];
|
|
const grouped = ["HeadMainDat", "CostMainDat", "MachMainDat"].includes(info.id);
|
|
let curGroup = "";
|
|
|
|
for (let i = 0; i < rows.length; i++) {
|
|
const r = rows[i];
|
|
if (grouped && Number(r.Index) > 0 && r.Code) curGroup = r.Code;
|
|
|
|
// 대상 자기 자신의 대표 행은 검색 결과에서 제외
|
|
if (r === target) continue;
|
|
|
|
let matched = false;
|
|
if (code && r.Code === code) {
|
|
matched = true;
|
|
} else if (!code && name) {
|
|
const rName = r.Name ? EstNative.normalize(r.Name) : "";
|
|
const rSize = r.Size ? EstNative.normalize(r.Size) : "";
|
|
if (rName === name && (!size || rSize === size)) {
|
|
matched = true;
|
|
}
|
|
}
|
|
|
|
if (matched) {
|
|
const rowName = r.Name || (r.LineData ? EstText.clean(r.LineData) : "");
|
|
results.push({
|
|
table: info.id,
|
|
tableLabel: info.label,
|
|
index: i,
|
|
group: curGroup,
|
|
code: r.Code || "",
|
|
name: rowName,
|
|
size: r.Size || "",
|
|
unite: r.Unite || "",
|
|
quantity: r.GN != null ? Number(r.GN) : null,
|
|
price: r.TC != null ? Number(r.TC) : null,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
function showUsesSearchDialog(targetRow) {
|
|
const { state, esc, number, showDialog, byId } = EstApp;
|
|
if (!targetRow) throw Error("사용위치를 검색할 행을 먼저 선택하세요.");
|
|
|
|
const rowName = targetRow.Name || (targetRow.LineData ? EstText.clean(targetRow.LineData) : "");
|
|
const results = findDataUsages(state.project, targetRow);
|
|
const title = `데이터 사용위치 검색: ${targetRow.Code ? `[${targetRow.Code}] ` : ""}${rowName || "선택 항목"}`;
|
|
|
|
let bodyHtml = "";
|
|
if (!results.length) {
|
|
bodyHtml =
|
|
'<p style="padding:15px;color:#666;">내역서, 일위대가, 단가산출, 기계경비에서 사용된 위치를 찾지 못했습니다.</p>';
|
|
} else {
|
|
bodyHtml = `
|
|
<div style="max-height:420px;overflow-y:auto;border:1px solid #ccc;margin-bottom:12px;">
|
|
<table class="sheet" style="width:100%;font-size:12px;">
|
|
<thead>
|
|
<tr>
|
|
<th>구분</th><th>코드</th><th>품명 / 산출근거</th><th>규격</th><th>단위</th><th>수량</th><th>금액</th><th>이동</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${results
|
|
.map(
|
|
(res) => `
|
|
<tr>
|
|
<td><b>${esc(res.tableLabel)}</b></td>
|
|
<td>${esc(res.code)}</td>
|
|
<td style="text-align:left;">${esc(res.name)}</td>
|
|
<td>${esc(res.size)}</td>
|
|
<td>${esc(res.unite)}</td>
|
|
<td style="text-align:right;">${res.quantity != null ? number(res.quantity) : "-"}</td>
|
|
<td style="text-align:right;">${res.price != null ? number(res.price) : "-"}</td>
|
|
<td>
|
|
<button class="tiny" data-action="goto-use-location" data-table="${res.table}" data-index="${res.index}" data-group="${esc(res.group)}">
|
|
이동
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`,
|
|
)
|
|
.join("")}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div style="font-size:12px;color:#555;">총 <b>${results.length}</b>곳에서 사용 중입니다. [이동] 버튼을 누르면 해당 화면과 행으로 이동합니다.</div>
|
|
`;
|
|
}
|
|
|
|
showDialog(title, bodyHtml);
|
|
|
|
const dialogContent = byId("dialog-content");
|
|
const clickHandler = (e) => {
|
|
const btn = e.target.closest('[data-action="goto-use-location"]');
|
|
if (!btn) return;
|
|
const targetTable = btn.dataset.table;
|
|
const targetIndex = Number(btn.dataset.index);
|
|
const targetGroup = btn.dataset.group || "";
|
|
|
|
byId("dialog").close();
|
|
dialogContent.removeEventListener("click", clickHandler);
|
|
|
|
EstApp.navigate(targetTable, targetGroup);
|
|
state.page = Math.floor(targetIndex / 100);
|
|
state.selection = new Set([targetIndex]);
|
|
EstApp.render();
|
|
};
|
|
dialogContent.addEventListener("click", clickHandler);
|
|
}
|
|
|
|
function install() {
|
|
// 필요한 경우 이벤트 리스너 등록
|
|
}
|
|
|
|
function toggleJcBimog(state) {
|
|
// REA ActionSetJCBimog (0xf17c10 / 0x10dba14): 경비성단가 지정/해제 -> SCheck 3 토글
|
|
const rows = state.project.tables[state.view],
|
|
indices = [...state.selection].sort((a, b) => a - b);
|
|
if (!rows || !["EstMainDat", "HeadMainDat"].includes(state.view))
|
|
throw Error("내역서 또는 일위대가 화면에서만 경비성단가를 지정/해제할 수 있습니다.");
|
|
if (!indices.length || indices.some((index) => !rows[index]))
|
|
throw Error("경비성단가를 지정/해제할 행을 선택하세요.");
|
|
|
|
// 원자적 검증: 한 행이라도 복원 정보가 없거나 거부되면 앞선 행도 변경되지 않도록 사전 검사
|
|
const plans = [];
|
|
for (const index of indices) {
|
|
const row = rows[index];
|
|
if (Number(row.SCheck) === 3) {
|
|
// 경비성 해제 -> 명시적 0/1/2 이력(_prevSCheck) 확인
|
|
let restoreVal =
|
|
row._prevSCheck !== undefined && [0, 1, 2].includes(Number(row._prevSCheck))
|
|
? Number(row._prevSCheck)
|
|
: null;
|
|
if (
|
|
restoreVal == null &&
|
|
row.SCheck_ !== undefined &&
|
|
[0, 1, 2].includes(Number(row.SCheck_))
|
|
) {
|
|
restoreVal = Number(row.SCheck_);
|
|
}
|
|
if (restoreVal == null) {
|
|
throw Error(
|
|
`행 ${index + 1}: 이전 비목 이력(0:비목별, 1:재료, 2:노무)이 없는 기존 경비성(3) 행은 임의 변환할 수 없습니다.`,
|
|
);
|
|
}
|
|
plans.push({ index, row, isToggleOn: false, restoreVal });
|
|
} else {
|
|
// 경비성 지정 -> 현재 SCheck(0, 1, 2)를 이력으로 보존
|
|
const cur = Number(row.SCheck) || 0;
|
|
plans.push({ index, row, isToggleOn: true, cur });
|
|
}
|
|
}
|
|
|
|
// 전수 검증 통과 후 일괄 적용
|
|
EstApp.change(() => {
|
|
for (const p of plans) {
|
|
if (p.isToggleOn) {
|
|
p.row._prevSCheck = p.cur;
|
|
EstBusinessActions.setEstimateClassification(
|
|
state.project,
|
|
p.row,
|
|
p.index,
|
|
3,
|
|
state.view,
|
|
);
|
|
} else {
|
|
delete p.row._prevSCheck;
|
|
EstBusinessActions.setEstimateClassification(
|
|
state.project,
|
|
p.row,
|
|
p.index,
|
|
p.restoreVal,
|
|
state.view,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function toggleSunGongsa(state) {
|
|
// REA popMainSetTotalSumLine: 순직공비 계산끝(순공사비) 지정/해제 -> LinkIndex 54 토글
|
|
const rows = state.project.tables.EstMainDat,
|
|
indices = [...state.selection].sort((a, b) => a - b);
|
|
if (state.view !== "EstMainDat" || !rows)
|
|
throw Error("내역서 화면에서만 순공사비 경계 행을 지정/해제할 수 있습니다.");
|
|
if (indices.length !== 1 || !rows[indices[0]])
|
|
throw Error("순공사비 경계 행으로 지정하거나 해제할 행 1개를 선택하세요.");
|
|
const index = indices[0],
|
|
row = rows[index];
|
|
|
|
// 연결 행 보호: 원천 연결(자재/노임/일위/단가/기계 등 LinkIndex 5,6,21,101~105) 또는
|
|
// 수량이 있는 행, 코드가 있는 상세 행은 순공사비 경계선으로 지정 불가 (원천/연결 파괴 차단)
|
|
const curLink = Number(row.LinkIndex) || 0;
|
|
if (curLink !== 54) {
|
|
if (
|
|
row.Code ||
|
|
Number(row.GN) > 0 ||
|
|
[5, 6, 21, 51, 52, 55, 57, 58, 61, 62, 67, 68, 101, 103, 104, 105].includes(curLink)
|
|
) {
|
|
throw Error(
|
|
"단가·수량 연결 행이나 기존 특수 공종 행은 순공사비 경계선으로 지정할 수 없습니다. 빈 제목/구분 행을 선택하세요.",
|
|
);
|
|
}
|
|
}
|
|
|
|
EstApp.change(() => {
|
|
if (curLink === 54) {
|
|
// 기존 54 해제 -> 보존된 원래 LinkIndex 또는 원래 유형이 확인되는 경우에만 복원
|
|
let origLink = row._origLinkIndex !== undefined ? Number(row._origLinkIndex) : null;
|
|
if (origLink == null && row.LinkIndex_ !== undefined && Number(row.LinkIndex_) !== 54) {
|
|
origLink = Number(row.LinkIndex_);
|
|
}
|
|
if (origLink == null) {
|
|
// 복원 정보가 없는 기존 원본 54 행: 원래 유형을 알 수 없으므로 임의 0 변환 차단
|
|
throw Error(
|
|
"기존 파일에 저장된 원본 순공사비(54) 행이거나 원래 유형을 확인할 수 없어 임의 0으로 변환할 수 없습니다.",
|
|
);
|
|
}
|
|
row.LinkIndex = origLink;
|
|
delete row._origLinkIndex;
|
|
if (row._origName !== undefined) {
|
|
row.Name = row._origName;
|
|
delete row._origName;
|
|
} else if (row.Name === "순공사비") {
|
|
row.Name = "공종";
|
|
}
|
|
} else {
|
|
// 기존 다른 54 행이 있으면 원래 상태로 안전 복원
|
|
for (const r of rows) {
|
|
if (Number(r.LinkIndex) === 54) {
|
|
let origL = r._origLinkIndex !== undefined ? Number(r._origLinkIndex) : null;
|
|
if (origL == null && r.LinkIndex_ !== undefined && Number(r.LinkIndex_) !== 54) {
|
|
origL = Number(r.LinkIndex_);
|
|
}
|
|
if (origL == null) {
|
|
throw Error(
|
|
"기존 원본 순공사비(54) 행의 원래 유형을 알 수 없어 다른 행으로 대체할 수 없습니다.",
|
|
);
|
|
}
|
|
r.LinkIndex = origL;
|
|
delete r._origLinkIndex;
|
|
if (r._origName !== undefined) {
|
|
r.Name = r._origName;
|
|
delete r._origName;
|
|
} else if (r.Name === "순공사비") {
|
|
r.Name = "공종";
|
|
}
|
|
}
|
|
}
|
|
row._origLinkIndex = curLink;
|
|
row._origName = row.Name ?? "";
|
|
row.LinkIndex = 54;
|
|
if (!row.Name || row.Name === "공종") row.Name = "순공사비";
|
|
}
|
|
});
|
|
}
|
|
|
|
return { render, findDataUsages, showUsesSearchDialog, install, toggleJcBimog, toggleSunGongsa };
|
|
})();
|