701 lines
24 KiB
JavaScript
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,
|
|
};
|
|
})();
|