- 원본 검색 창(TFRMSERCHPROJECT)처럼 7종 자료를 검색어로 찾아 여러 개 골라 넣기 - 원본 데이타 변경 확인(TFRMDBLCLICKRTN)처럼 선택 행 연결 바꾸기, 같은 코드 전체 변경 선택 - 넣거나 바꾼 행만 원천 단가 재조회, 실패 시 되돌림 - 시험 test_est_pick.py 추가 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
239 lines
9.3 KiB
JavaScript
239 lines
9.3 KiB
JavaScript
// 원본 TFRMSERCHPROJECT(검색어·여러개선택·동시검색·적용)와 TFRMDBLCLICKRTN(데이타 변경 확인)을 옮긴 단가 고르기.
|
|
// 내역서·일위대가 행에 기초자료를 골라 넣거나 선택 행의 연결을 바꾼다. 단가산출 산출근거(LineData)는 대상이 아니다.
|
|
const EstPick = (() => {
|
|
const kinds = [
|
|
[5, "StuffcDat", "자재"],
|
|
[6, "LaborcDat", "노임"],
|
|
[105, "MachTitleDat", "기계경비"],
|
|
[103, "HeadTitleDat", "일위대가"],
|
|
[104, "CostTitleDat", "단가산출"],
|
|
[101, "EstiMateDat", "견적"],
|
|
[21, "SilJukDat", "실적"],
|
|
];
|
|
const views = ["EstMainDat", "HeadMainDat"];
|
|
const limit = 300;
|
|
const text = (value) =>
|
|
String(value ?? "")
|
|
.normalize("NFKC")
|
|
.toLowerCase()
|
|
.replace(/\s+/g, "");
|
|
const amount = (row) => {
|
|
const total = Number(row.TC) || 0;
|
|
return total || ["NC", "GC", "JC", "SC"].reduce((sum, key) => sum + (Number(row[key]) || 0), 0);
|
|
};
|
|
|
|
function items(project, kind) {
|
|
const list = [];
|
|
for (const [k, table, label] of kinds) {
|
|
if (kind && k !== kind) continue;
|
|
for (const row of project.tables[table] || [])
|
|
if (row.Code && Number(row.Index ?? 1) >= 0)
|
|
list.push({
|
|
kind: k,
|
|
label,
|
|
Code: row.Code,
|
|
Name: row.Name ?? "",
|
|
Size: row.Size ?? "",
|
|
Unite: row.Unite ?? "",
|
|
price: amount(row),
|
|
});
|
|
}
|
|
return list;
|
|
}
|
|
|
|
function search(project, kind, query) {
|
|
const words = String(query || "")
|
|
.split(/\s+/)
|
|
.map(text)
|
|
.filter(Boolean);
|
|
const found = items(project, kind).filter((item) => {
|
|
const haystack = text(`${item.Code} ${item.Name} ${item.Size}`);
|
|
return words.every((word) => haystack.includes(word));
|
|
});
|
|
return { rows: found.slice(0, limit), total: found.length };
|
|
}
|
|
|
|
function guard(view, group, picks) {
|
|
if (!views.includes(view))
|
|
throw Error("내역서·일위대가 화면에서만 단가를 골라 넣을 수 있습니다.");
|
|
if (!picks.length) throw Error("넣을 자료를 하나 이상 고르세요.");
|
|
if (view === "HeadMainDat" && picks.some((p) => p.kind === 103 && p.Code === group))
|
|
throw Error("일위대가 자신을 하위 자료로 넣을 수 없습니다.");
|
|
}
|
|
|
|
function linked(base, pick) {
|
|
return {
|
|
...base,
|
|
Code: pick.Code,
|
|
Name: pick.Name,
|
|
Size: pick.Size,
|
|
Unite: pick.Unite,
|
|
LinkIndex: pick.kind,
|
|
};
|
|
}
|
|
|
|
function insert(project, view, at, group, picks) {
|
|
guard(view, group, picks);
|
|
const rows = project.tables[view];
|
|
const added = [];
|
|
for (const pick of picks) {
|
|
const row = linked(EstModel.newRow(view, rows, project), pick);
|
|
rows.splice(at + 1 + added.length, 0, row);
|
|
added.push(at + 1 + added.length);
|
|
}
|
|
try {
|
|
refresh(project, view, added);
|
|
} catch (error) {
|
|
rows.splice(at + 1, added.length);
|
|
throw error;
|
|
}
|
|
return added;
|
|
}
|
|
|
|
function refresh(project, view, indices) {
|
|
// 내역·일위대가 행은 요청한 행만 원천 단가를 다시 읽는다(행 비목 지정과 같은 경로).
|
|
let failures = null;
|
|
const done = EstNative.calculate(
|
|
EstModel.validate(structuredClone(project)),
|
|
view === "HeadMainDat"
|
|
? { refreshHeadRows: indices, onHeadRefresh: (errors) => (failures = errors) }
|
|
: { refreshEstimateRows: indices, onEstimateRefresh: (errors) => (failures = errors) },
|
|
);
|
|
if (!failures) throw Error("고른 자료의 단가를 다시 읽지 못했습니다.");
|
|
if (failures.length) throw Error(failures.join(" / "));
|
|
for (const index of indices)
|
|
for (const key of EstMath.units)
|
|
project.tables[view][index][key] = done.tables[view][index][key];
|
|
}
|
|
|
|
function replaceable(target) {
|
|
if (
|
|
!target ||
|
|
Number(target.Index) > 0 ||
|
|
[54, 55, 57, 58, 61, 62, 67, 68].includes(Number(target.LinkIndex))
|
|
)
|
|
throw Error("일반 내역 행 하나를 선택하세요. 제목·요율·순공사비 행은 바꿀 수 없습니다.");
|
|
}
|
|
|
|
function replace(project, view, index, group, pick, all) {
|
|
guard(view, group, [pick]);
|
|
const rows = project.tables[view],
|
|
target = rows[index];
|
|
replaceable(target);
|
|
const old = { kind: Number(target.LinkIndex) || 0, Code: target.Code };
|
|
const changed = [],
|
|
backup = structuredClone(rows);
|
|
rows.forEach((row, i) => {
|
|
const same =
|
|
i === index ||
|
|
(all &&
|
|
old.Code &&
|
|
row.Code === old.Code &&
|
|
Number(row.LinkIndex) === old.kind &&
|
|
Number(row.Index ?? 0) <= 0);
|
|
if (!same) return;
|
|
Object.assign(row, linked({}, pick));
|
|
changed.push(i);
|
|
});
|
|
try {
|
|
refresh(project, view, changed);
|
|
} catch (error) {
|
|
project.tables[view] = backup;
|
|
throw error;
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
function cell(value) {
|
|
return EstApp.esc(value);
|
|
}
|
|
|
|
function results(state) {
|
|
const pick = state.pick,
|
|
{ rows, total } = search(state.project, pick.kind, pick.query);
|
|
pick.rows = rows;
|
|
const body = rows
|
|
.map(
|
|
(item, i) =>
|
|
`<tr><td><input type="checkbox" name="pick-row" value="${i}"></td><td>${cell(item.label)}</td><td>${cell(item.Code)}</td><td style="text-align:left">${cell(item.Name)}</td><td>${cell(item.Size)}</td><td>${cell(item.Unite)}</td><td style="text-align:right">${EstApp.number(item.price)}</td></tr>`,
|
|
)
|
|
.join("");
|
|
return `<div style="max-height:380px;overflow-y:auto;border:1px solid #ccc"><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></tr></thead><tbody>${body}</tbody></table></div><p>찾은 자료 ${total}건${total > limit ? ` 중 앞 ${limit}건 표시 — 검색어를 더 넣으세요` : ""}</p>`;
|
|
}
|
|
|
|
function open(state) {
|
|
if (!views.includes(state.view))
|
|
throw Error("내역서·일위대가 화면에서만 단가를 골라 넣을 수 있습니다.");
|
|
state.pick ||= { kind: 0, query: "" };
|
|
const options = [[0, "전체 동시검색"], ...kinds.map(([k, , label]) => [k, label])]
|
|
.map(
|
|
([k, label]) =>
|
|
`<option value="${k}" ${k === state.pick.kind ? "selected" : ""}>${cell(label)}</option>`,
|
|
)
|
|
.join("");
|
|
EstApp.showDialog(
|
|
"단가 고르기",
|
|
`<div class="form-fields"><label>자료<select name="pick-kind">${options}</select></label><label>검색어<input name="pick-query" value="${cell(state.pick.query)}"></label></div><button data-action="pick-search">검색</button><div id="pick-results">${results(state)}</div><button data-action="pick-insert">선택 행 아래에 넣기</button> <button data-action="pick-replace">선택 행을 이것으로 바꾸기</button>`,
|
|
);
|
|
}
|
|
|
|
function chosen(state) {
|
|
return [...document.querySelectorAll('#dialog-content [name="pick-row"]:checked')].map(
|
|
(node) => state.pick.rows[Number(node.value)],
|
|
);
|
|
}
|
|
|
|
function confirmReplace(target, pick) {
|
|
const line = (row) =>
|
|
`${row.Name || ""} / ${row.Size || ""} / ${row.Unite || ""}${row.Code ? ` [${row.Code}]` : ""}`;
|
|
return confirm(
|
|
`데이타 변경 확인\n변경 전: ${line(target)}\n변경 후: ${line(pick)}\n\n변경하시겠습니까?`,
|
|
);
|
|
}
|
|
|
|
function handle(name, state) {
|
|
if (name === "pick-open") open(state);
|
|
else if (name === "pick-search") {
|
|
state.pick.kind = Number(document.querySelector('#dialog-content [name="pick-kind"]').value);
|
|
state.pick.query = document.querySelector('#dialog-content [name="pick-query"]').value;
|
|
EstApp.byId("pick-results").innerHTML = results(state);
|
|
} else if (name === "pick-insert") {
|
|
const picks = chosen(state),
|
|
rows = state.project.tables[state.view];
|
|
guard(state.view, state.group, picks);
|
|
const group = EstNative.groups(rows).get(state.group);
|
|
const at =
|
|
[...state.selection].sort((a, b) => a - b).at(-1) ??
|
|
(group ? group.start + group.rows.length - 1 : rows.length - 1);
|
|
EstApp.byId("dialog").close();
|
|
EstApp.change((project) => {
|
|
state.selection = new Set(insert(project, state.view, at, state.group, picks));
|
|
});
|
|
state.notice = `${picks.length}개 자료를 넣었습니다.`;
|
|
} else if (name === "pick-replace") {
|
|
const picks = chosen(state),
|
|
index = [...state.selection][0];
|
|
if (picks.length !== 1 || state.selection.size !== 1)
|
|
throw Error("바꿀 행 하나와 자료 하나를 고르세요.");
|
|
const target = state.project.tables[state.view][index];
|
|
guard(state.view, state.group, picks);
|
|
replaceable(target);
|
|
if (!confirmReplace(target, picks[0])) return true;
|
|
const all =
|
|
Boolean(target.Code) &&
|
|
confirm(
|
|
`같은 코드(${target.Code})를 쓰는 이 표의 다른 행도 함께 바꾸시겠습니까?\n[취소] 현재 행만 변경`,
|
|
);
|
|
EstApp.byId("dialog").close();
|
|
let changed = [];
|
|
EstApp.change((project) => {
|
|
changed = replace(project, state.view, index, state.group, picks[0], all);
|
|
});
|
|
state.notice = `${changed.length}개 행의 연결을 바꿨습니다.`;
|
|
} else return false;
|
|
return true;
|
|
}
|
|
|
|
return { kinds, items, search, insert, replace, handle };
|
|
})();
|