// 원본 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) => `${cell(item.label)}${cell(item.Code)}${cell(item.Name)}${cell(item.Size)}${cell(item.Unite)}${EstApp.number(item.price)}`, ) .join(""); return `
${body}
구분코드품명규격단위단가

찾은 자료 ${total}건${total > limit ? ` 중 앞 ${limit}건 표시 — 검색어를 더 넣으세요` : ""}

`; } 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]) => ``, ) .join(""); EstApp.showDialog( "단가 고르기", `
${results(state)}
`, ); } 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 }; })();