Files
Aislo/tools/est-test/native-block-actions.js
T

701 lines
24 KiB
JavaScript

// REA popMainBlockCut, popMainBlockCopy, popMainBlockPaste, popMainBlockDel 및 블럭 범위 지정 (ActionBlockOneSet, 블럭시작/끝)
// 및 popMainSetStr (산출근거 주석 지정/해제), TFRMWONGABASE popupTotal (공사비역산 자리수 맞추기)
// 및 mnuLevelUp, mnuLevelDown, mnuCostLevelUp, mnuCostLevelDown (레벨 일괄 증가/감소)
const EstBlockActions = (() => {
let blockStart = null;
let blockTable = null;
function setBlockStart(state) {
const rows = state.project.tables[state.view];
if (!rows) throw Error("현재 화면에서는 블럭을 지정할 수 없습니다.");
const selected = [...state.selection];
if (!selected.length) throw Error("블럭 시작 위치로 지정할 행을 선택하세요.");
blockStart = selected[0];
blockTable = state.view;
state.notice = `${blockStart + 1}행이 블럭 시작점으로 설정되었습니다. (블럭 끝 행을 선택 후 '블럭 끝'을 누르세요)`;
}
function setBlockEnd(state) {
const rows = state.project.tables[state.view];
if (!rows) throw Error("현재 화면에서는 블럭을 지정할 수 없습니다.");
if (blockStart == null || blockTable !== state.view) {
throw Error("먼저 '블럭 시작'을 지정하세요.");
}
const selected = [...state.selection];
if (!selected.length) throw Error("블럭 끝 위치로 지정할 행을 선택하세요.");
const blockEnd = selected[0];
const start = Math.min(blockStart, blockEnd);
const end = Math.max(blockStart, blockEnd);
state.selection.clear();
for (let i = start; i <= end; i++) {
state.selection.add(i);
}
state.notice = `${start + 1}행부터 ${end + 1}행까지 총 ${end - start + 1}개 행이 블럭으로 선택되었습니다.`;
blockStart = null;
blockTable = null;
}
function clearBlockSelection(state) {
state.selection.clear();
blockStart = null;
blockTable = null;
state.notice = "선택이 해제되었습니다.";
}
function selectAllRows(state) {
const rows = state.project.tables[state.view];
if (!rows) throw Error("현재 화면에서는 전체 선택을 할 수 없습니다.");
state.selection.clear();
for (let i = 0; i < rows.length; i++) {
state.selection.add(i);
}
state.notice = `전체 ${rows.length}개 행이 선택되었습니다.`;
}
function deleteBlockRows(state) {
const rows = state.project.tables[state.view];
if (!rows) throw Error("현재 화면에서는 블럭 삭제를 할 수 없습니다.");
const indexes = new Set([...state.selection]);
if (!indexes.size) throw Error("삭제할 블럭 행을 먼저 선택하세요.");
// 호표/기계경비 그룹이 있는 경우 호표 제목 행 포함 시 전체 호표 행 포함
if (["HeadMainDat", "CostMainDat", "MachMainDat"].includes(state.view)) {
for (const group of EstNative.groups(rows).values()) {
if (indexes.has(group.start)) {
for (let i = group.start; i < group.start + group.rows.length; i++) {
indexes.add(i);
}
}
}
}
const codes = new Set(
[...indexes]
.map((i) => rows[i]?.Code)
.filter(Boolean)
.map((c) => String(c).trim()),
);
const kind = Number(
Object.entries(EstNative.sourceTables).find(([, table]) => table === state.view)?.[0] ||
{ HeadMainDat: 103, CostMainDat: 104, MachMainDat: 105 }[state.view],
);
if (
kind &&
Object.entries(state.project.tables).some(([table, list]) =>
list.some((row, i) => {
if (table === state.view && indexes.has(i)) return false;
const rLink = Number(row.LinkIndex);
const rCode = row.Code ? String(row.Code).trim() : "";
return rLink === kind && rCode && codes.has(rCode);
}),
)
) {
throw Error("다른 항목에서 사용 중입니다. 연결을 먼저 해제하세요.");
}
if (!confirm(`${indexes.size}개 행을 블럭 삭제하시겠습니까?`)) return;
EstApp.change(() => {
[...indexes].sort((a, b) => b - a).forEach((i) => rows.splice(i, 1));
state.selection.clear();
blockStart = null;
blockTable = null;
});
}
function toggleCostComment(state) {
// REA popMainSetStr: 단가산출 산출근거 행을 주석(Index = -1)으로 지정 또는 일반 산출식으로 복원
const rows = state.project.tables.CostMainDat;
const indices = [...state.selection].sort((a, b) => a - b);
if (state.view !== "CostMainDat" || !rows) {
throw Error("단가산출 화면에서만 산출근거 행을 주석으로 지정할 수 있습니다.");
}
if (!indices.length || indices.some((i) => !rows[i])) {
throw Error("주석으로 지정하거나 해제할 산출근거 행을 선택하세요.");
}
const plans = [];
for (const index of indices) {
const row = rows[index];
const curIndex = Number(row.Index) || 0;
if (curIndex > 0) {
throw Error(
`행 ${index + 1}: 호표 제목 행은 주석으로 지정할 수 없습니다. 상세 산출근거 행을 선택하세요.`,
);
}
if (curIndex === -20) {
throw Error(`행 ${index + 1}: 소계 구분선 행은 주석으로 지정할 수 없습니다.`);
}
const curSCheck = Number(row.SCheck) || 0;
const isComment = curIndex === -1 || curSCheck === -1;
if (isComment) {
// 이미 주석인 행 -> 복원
let restoreIndex = 0;
if (row._origIndex !== undefined) {
restoreIndex = Number(row._origIndex);
} else if (row.Index_ !== undefined) {
restoreIndex = Number(row.Index_);
}
let restoreSCheck = null;
if (row._origSCheck !== undefined) {
restoreSCheck = Number(row._origSCheck);
} else if (row.SCheck_ !== undefined) {
restoreSCheck = Number(row.SCheck_);
} else if ([-1, -2, -3, -5].includes(Number(row.OCheck))) {
restoreSCheck = Number(row.OCheck);
}
plans.push({
index,
row,
isComment: false,
restoreIndex,
restoreSCheck,
});
} else {
// 일반 상세 행 또는 비목 행(-2, -3, -5 등) -> 주석(-1)으로 전환
plans.push({
index,
row,
isComment: true,
origIndex: curIndex,
origSCheck: row.SCheck !== undefined ? Number(row.SCheck) : null,
});
}
}
EstApp.change(() => {
for (const p of plans) {
if (p.isComment) {
p.row._origIndex = p.origIndex;
if (p.origSCheck !== null) {
p.row._origSCheck = p.origSCheck;
}
p.row.Index = -1;
p.row.SCheck = -1;
} else {
delete p.row._origIndex;
delete p.row._origSCheck;
p.row.Index = p.restoreIndex;
if (p.restoreSCheck !== null) {
p.row.SCheck = p.restoreSCheck;
} else {
delete p.row.SCheck;
}
}
}
});
}
function alignWongaTotalAction(state) {
// REA TFRMWONGABASE popupTotal: 공사비역산 자리수 맞추기
const rows = state.project.tables.Wonga;
if (!rows || !rows.length) {
throw Error("원가계산서 자료가 없습니다.");
}
const targetRow = rows.find((r) => Number(r.LinkIndex) === 55);
const curTotal = targetRow ? Number(targetRow.Cost) || 0 : 0;
EstActions.form(
"공사비역산 자리수 맞추기",
[
{
name: "targetCost",
label: "목표 도급공사비 (원)",
type: "number",
value: curTotal,
},
{
name: "unitDigits",
label: "단단위 절하 기준",
value: "3",
choices: [
["0", "원 단위 (절하 없음)"],
["1", "십원 단위 (10원 절하)"],
["2", "백원 단위 (100원 절하)"],
["3", "천원 단위 (1,000원 절하)"],
["4", "만원 단위 (10,000원 절하)"],
],
},
],
({ targetCost, unitDigits }) => {
EstApp.change(() => {
const direct = state.project.calculation?.direct || {};
const bases = {
NC: Number(direct.NC) || 0,
GC: Number(direct.GC) || 0,
JC: Number(direct.JC) || 0,
SC: Number(direct.SC) || 0,
TC: Number(direct.TC) || 0,
노무비: Number(direct.NC) || 0,
재료비: Number(direct.GC) || 0,
경비: Number(direct.JC) || 0,
실적: Number(direct.SC) || 0,
직접공사비: Number(direct.TC) || 0,
};
EstWonga.alignTotal(rows, targetCost, unitDigits, state.project.options, bases);
state.project.options.TotalFix = true;
});
},
);
}
function resetChangeAction(state) {
// REA mnuChangeInit: 설계변경 초기화 (확정된 당초 내역으로 완전 복원 및 변경 사본 제거)
const project = state.project;
// 당초 확정 정보(baseline)가 없는 불완전 상태에서의 복원 시도는 데이터 유실 위험이 있으므로 엄격 차단
if (!project.baseline || !project.baseline.tables) {
throw Error(
"확정된 당초 내역(baseline)이 없습니다. 완전 복원이 불가능하여 초기화를 진행할 수 없습니다.",
);
}
if (project.options.Mode !== "설계변경" && project.baseline == null) {
throw Error("설계변경 모드가 아닙니다.");
}
// 1) 수량 연결(quantities, quantitySheets, excelQuantityLinks) 참조 무결성 사전 검사
const baselineEstMain = project.baseline.tables.EstMainDat || [];
// 수량산출서 연결 검사
const qtyItems = [];
if (Array.isArray(project.quantities)) {
qtyItems.push(...project.quantities);
}
if (Array.isArray(project.quantitySheets)) {
for (const s of project.quantitySheets) {
if (Array.isArray(s.items)) qtyItems.push(...s.items);
}
}
for (const item of qtyItems) {
const code = item.TargetCode ?? item.LinkCode;
const key = item.TargetKey;
if (!code && !key) continue;
const targets = baselineEstMain.filter(
(row) =>
!Number(row.Level) &&
(key ? row.PkID != null && `id:${row.PkID}` === key : row.Code === code),
);
if (targets.length !== 1) {
throw Error(
`수량산출 연결 무결성 오류: 신규 행을 참조하는 수량 연결(${key || code})이 존재하여 당초 내역 복원 시 연결이 끊어집니다. 해당 수량 연결을 먼저 해제하거나 변경하세요.`,
);
}
}
// Excel 수량 연결 검사 (안정된 고유 식별자 PkID 및 항목 내용 동일성 엄격 검증)
if (Array.isArray(project.excelQuantityLinks) && project.excelQuantityLinks.length > 0) {
const currentEstMain = project.tables.EstMainDat || [];
const currentRowsByPkID = new Map();
currentEstMain.forEach((r, idx) => {
if (r.PkID != null) currentRowsByPkID.set(Number(r.PkID), { row: r, index: idx });
});
const baselineRowsByPkID = new Map();
baselineEstMain.forEach((r, idx) => {
if (r.PkID != null) baselineRowsByPkID.set(Number(r.PkID), { row: r, index: idx });
});
for (const link of project.excelQuantityLinks) {
if (!link || link.target == null) continue;
const targetId = Number(link.target);
// 1) 현재 작업본에 대상 행이 존재하는지 확인
const curInfo = currentRowsByPkID.get(targetId);
if (!curInfo) {
throw Error(
`Excel 수량 연결 무결성 오류: 연결 대상 행(PkID ${targetId})이 현재 내역에 없습니다. 해당 연결을 먼저 정리하세요.`,
);
}
// 2) 당초 확정본(baseline)에 대상 행이 존재하는지 확인 (신규 행 참조 차단)
const baseInfo = baselineRowsByPkID.get(targetId);
if (!baseInfo) {
throw Error(
`Excel 수량 연결 무결성 오류: 신규 내역 행(PkID ${targetId}, ${curInfo.row.Code || curInfo.row.Name || ""})을 참조하는 Excel 연결(${link.file} ${link.cell})이 존재하여 당초 내역 복원 시 연결이 끊어집니다. 해당 연결을 먼저 해제하세요.`,
);
}
// 3) 동일성(Identity) 입증: Code, Name, Size, Unite 및 상세행 조건이 당초 행과 일치하는지 대조
const curRow = curInfo.row;
const baseRow = baseInfo.row;
const isSameIdentity =
String(curRow.Code || "") === String(baseRow.Code || "") &&
String(curRow.Name || "") === String(baseRow.Name || "") &&
String(curRow.Size || "") === String(baseRow.Size || "") &&
String(curRow.Unite || "") === String(baseRow.Unite || "") &&
Number(curRow.Level || 0) === Number(baseRow.Level || 0);
if (!isSameIdentity) {
throw Error(
`Excel 수량 연결 무결성 오류: 연결 대상 행(PkID ${targetId})의 내용(${curRow.Name})이 당초 행(${baseRow.Name})과 일치하지 않아 다른 항목을 가리키는 위험이 있습니다. 동일성이 입증되지 않아 초기화를 차단합니다.`,
);
}
}
}
if (
!confirm(
"설계변경 작업을 초기화하고 당초 내역 상태로 되돌리시겠습니까?\n당초 사본에 포함되지 않은 기계·기초단가 자료는 현재 상태로 유지됩니다.",
)
) {
return;
}
// 설계변경 당초 사본에 포함된 테이블만 복원한다.
const changeTables = [
"EstMainDat",
"HeadMainDat",
"HeadTitleDat",
"CostMainDat",
"CostTitleDat",
"MachMainDat",
"MachTitleDat",
"Wonga",
];
// 설계변경 관련 특정 필드 목록 (EstBusiness.baseline 스펙 일치)
const baselineFields = [
"Code",
"Name",
"Size",
"Unite",
"GN",
"TU",
"TC",
"NU",
"GU",
"JU",
"SU",
"NC",
"GC",
"JC",
"SC",
"LineData",
"VSusic",
"Cost",
];
EstApp.change(() => {
// 1) baseline.tables에 존재하는 대상 테이블만 복원한다.
for (const tName of changeTables) {
if (project.baseline.tables[tName]) {
project.tables[tName] = structuredClone(project.baseline.tables[tName]);
}
}
// 2) 대상 테이블의 행들에서만 설계변경 관련 _ 접미사 필드 제거 (다른 업무 원본 필드, _origLinkIndex, _prevSCheck 등 완벽 보존)
for (const tName of changeTables) {
const rows =
tName !== "MachMainDat" && project.baseline.tables[tName] && project.tables[tName];
if (!Array.isArray(rows)) continue;
for (const row of rows) {
row.NewItem = false;
for (const f of baselineFields) {
delete row[`${f}_`];
}
}
}
// 3) baseline 제거 및 모드 환원
delete project.baseline;
project.options.Mode = "내역서";
});
state.notice = "설계변경이 초기화되어 당초 내역 상태로 복원되었습니다.";
}
function resetWongaAction(state) {
// REA TFRMWONGABASE popupInit: 원가계산서 항목 초기화
const rows = state.project.tables.Wonga;
if (!rows || !rows.length) {
throw Error("원가계산서 자료가 없습니다.");
}
if (
!confirm(
"원가계산서 항목을 초기화하시겠습니까?\n(공사비역산 고정액이 해제되고 당초 수식 및 기준 단가로 재계산됩니다)",
)
) {
return;
}
const direct = state.project.calculation?.direct || {};
const bases = {
NC: Number(direct.NC) || 0,
GC: Number(direct.GC) || 0,
JC: Number(direct.JC) || 0,
SC: Number(direct.SC) || 0,
TC: Number(direct.TC) || 0,
노무비: Number(direct.NC) || 0,
재료비: Number(direct.GC) || 0,
경비: Number(direct.JC) || 0,
실적: Number(direct.SC) || 0,
직접공사비: Number(direct.TC) || 0,
};
const restored = structuredClone(rows);
EstWonga.resetWonga(restored, state.project.options, bases);
EstApp.change(() => {
state.project.tables.Wonga = restored;
state.project.options.TotalFix = false;
});
state.notice = "원가계산서 항목이 초기화되었습니다.";
}
function resetProgressInputsAction(state) {
// REA mnuFileAccomInit: 기성 회차 입력 초기화 (선택된 회차의 금회/사정 입력 및 모드를 초기화)
const project = state.project;
if (!project.progress || !project.progress.length) {
throw Error("기성 회차 자료가 없습니다.");
}
const round = project.progress.find((r) => r.id === state.roundId);
if (!round) {
throw Error("선택된 기성 회차가 없습니다.");
}
if (round.locked) {
throw Error("확정된 회차는 입력을 초기화할 수 없습니다. 확정을 먼저 해제하세요.");
}
if (
!confirm(
`[${round.name || "현재 회차"}]의 모든 금회 기성 및 사정 입력을 초기화하시겠습니까?\n(입력된 기성 수량과 비율이 모두 지워집니다)`,
)
) {
return;
}
EstApp.change(() => {
round.inputs = {};
delete round.result;
}, false);
state.notice = `[${round.name || "현재 회차"}]의 기성 입력이 초기화되었습니다.`;
}
function resetAnnualAction(state) {
// REA 연차공사: 등록된 차수 배분 설정 초기화 (웹 전용 보조 기능, 원본 동등성 미확인)
// 주의: REA 원본의 popInit은 단일 차수 수식/단가 초기화일 가능성이 있어 전체 차수 삭제와의 동등성은 미확인입니다.
// 본 기능은 등록된 연차 차수 목록을 비우는 웹 전용 보조 기능으로 명확히 구분하여 제공합니다.
const project = state.project;
if (!project.annual || !project.annual.length) {
throw Error("연차공사 차수 자료가 없습니다.");
}
if (
!confirm(
"등록된 모든 연차공사 차수와 배분 수식을 초기화하시겠습니까?\n(웹 보조 기능: 기존 내역서 및 원가계산서 데이터는 유지됩니다)",
)
) {
return;
}
EstApp.change(() => {
project.annual = [];
}, false);
state.notice = "연차공사 차수 및 배분 설정이 초기화되었습니다 (웹 보조 기능).";
}
function resetEscalationAction(state) {
// REA 물가변동: 적용제외 수량 및 입력 초기화 (TFRMESCALATION popInit 스펙 기반)
const rows = state.project.tables.EstMainDat;
if (!rows || !rows.length) {
throw Error("내역서 자료가 없습니다.");
}
if (
!confirm(
"물가변동 적용제외 수량과 입력값을 초기화하시겠습니까?\n(예정·실행·기성·수동제외 수량이 초기화되고 적용제외 자동계산이 기본값으로 환원됩니다)",
)
) {
return;
}
EstApp.change(() => {
for (const row of rows) {
delete row.EscPlanned;
delete row.EscActual;
delete row.EscProgress;
delete row.EscManualQuantity;
delete row.EscConstruction;
delete row.EscPS;
delete row.EscBefore;
delete row.EscAuto;
}
delete state.project.options.EscDesignTotal;
}, false);
state.notice = "물가변동 적용제외 수량 및 설정이 초기화되었습니다.";
}
function adjustLevels(state, delta) {
// REA mnuLevelUp, mnuLevelDown, mnuCostLevelUp, mnuCostLevelDown: 선택 행 레벨 일괄 증가/감소
const rows = state.project.tables[state.view];
if (
!rows ||
!["EstMainDat", "CostMainDat", "HeadMainDat", "MachMainDat"].includes(state.view)
) {
throw Error("내역서, 단가산출, 일위대가, 기계경비 화면에서만 레벨을 조정할 수 있습니다.");
}
const selected = [...state.selection].sort((a, b) => a - b);
if (!selected.length) {
throw Error("레벨을 조정할 행을 선택하세요.");
}
EstApp.change(() => {
for (const idx of selected) {
const r = rows[idx];
if (!r) continue;
const curLevel = Number(r.Level) || 0;
// Level 범위: 0 ~ 9
const newLevel = Math.max(0, Math.min(9, curLevel + delta));
r.Level = newLevel;
}
});
state.notice = `${selected.length}개 행의 레벨이 일괄 ${delta > 0 ? "증가" : "감소"}되었습니다.`;
}
function pageRows(state, rows, key, eligible) {
const query = String(state.query || "")
.trim()
.toLowerCase();
const filtered = rows
.map((row, index) => ({ row, index }))
.filter(
({ row }) =>
eligible(row) &&
(!query ||
["Code", "Name", "Size", "Unite"].some((field) =>
String(row[field] ?? "")
.toLowerCase()
.includes(query),
)),
);
const pages = Math.max(1, Math.ceil(filtered.length / 100));
state[key] = Math.max(0, Math.min(Number(state[key]) || 0, pages - 1));
return {
rows: filtered.slice(state[key] * 100, state[key] * 100 + 100),
pages,
total: filtered.length,
};
}
function progressPage(state, rows) {
return pageRows(
state,
rows,
"progressPage",
(row) =>
!EstMath.num(row.Level) &&
(row.Code || row.Name || [54, 55, 57, 58, 62, 67].includes(Number(row.LinkIndex))),
);
}
function escalationPage(state, rows) {
return pageRows(
state,
rows,
"escalationPage",
(row) =>
(row.Code || row.Name) &&
!EstMath.num(row.Level) &&
![54, 55, 58, 59].includes(Number(row.LinkIndex)),
);
}
function changePage(state, rows) {
return pageRows(state, rows, "changePage", (row) => row.Code || row.Name);
}
function handle(name, state) {
if (name === "progress-prev" || name === "progress-next") {
state.progressPage = (Number(state.progressPage) || 0) + (name === "progress-next" ? 1 : -1);
EstApp.render();
return true;
}
if (name === "escalation-prev" || name === "escalation-next") {
state.escalationPage =
(Number(state.escalationPage) || 0) + (name === "escalation-next" ? 1 : -1);
EstApp.render();
return true;
}
if (name === "change-prev" || name === "change-next") {
state.changePage = (Number(state.changePage) || 0) + (name === "change-next" ? 1 : -1);
EstApp.render();
return true;
}
if (name === "block-start") {
setBlockStart(state);
return true;
}
if (name === "block-end") {
setBlockEnd(state);
return true;
}
if (name === "block-clear") {
clearBlockSelection(state);
return true;
}
if (name === "select-all-rows") {
selectAllRows(state);
return true;
}
if (name === "block-del") {
deleteBlockRows(state);
return true;
}
if (name === "toggle-cost-comment") {
toggleCostComment(state);
return true;
}
if (name === "align-wonga-total") {
alignWongaTotalAction(state);
return true;
}
if (name === "reset-escalation") {
resetEscalationAction(state);
return true;
}
if (name === "reset-annual") {
resetAnnualAction(state);
return true;
}
if (name === "reset-round-inputs") {
resetProgressInputsAction(state);
return true;
}
if (name === "reset-wonga") {
resetWongaAction(state);
return true;
}
if (name === "level-batch-up") {
adjustLevels(state, 1);
return true;
}
if (name === "reset-change") {
resetChangeAction(state);
return true;
}
if (name === "level-batch-down") {
adjustLevels(state, -1);
return true;
}
return false;
}
return {
setBlockStart,
setBlockEnd,
clearBlockSelection,
selectAllRows,
deleteBlockRows,
toggleCostComment,
alignWongaTotalAction,
resetEscalationAction,
resetAnnualAction,
resetProgressInputsAction,
resetWongaAction,
resetChangeAction,
adjustLevels,
progressPage,
escalationPage,
changePage,
handle,
};
})();