- 코드 등록·삭제·변경·병합·데이타병합·일괄변경·검색·동일코드·풀어넣기 끝전 등(ui_code · ui_merge · api_ui_code) - 코드 붙은 % 줄 = 같은 상위 공종 앞 말단 줄 비목 합 × 율(server pct_row_base · calc.js) · 저장 때 다시 셈(stc_save _pct_rows) — 실무 7건 저장값 그대로 - 내역 줄 단가 ≠ 단가목록이면 줄 고정 단가(fixedUnit) - 메뉴 94개 눌러 오류 0 · 시험 502 통과 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
325 lines
16 KiB
JavaScript
325 lines
16 KiB
JavaScript
/**
|
|
* STMATE w 8.0 — 글자 입력 도우미·내역 보조 창 끼워 넣기 (scr_ui_text.js)
|
|
* 특수문자(mMinMsg_ · 14종 · 문자 입력후 종료 · 다중문자 동시입력) · 사용자문자 편집(wM_Edit_USER · 검색명칭/입력내용/적용)
|
|
* · 메모 입력(wInDlgMemo · Ok/Cancel/Load/Clear — 내역 행 DES1·DES2) · 동일코드 추려보기(wSameCode «추려보기» · N번 사용중 · 더블클릭 → 내역 위치)
|
|
* · 실적단가 → 일식단가 변환등록(wM_Conv_A2W · 항목선택 → 비목율 → 변환코드 → 변환/생성)
|
|
* 글자 넣는 자리 = 마지막으로 손댄 입력칸·글상자(커서 자리) · 사용자문자 저장 = 브라우저(localStorage, 원본 USR_SYNC.DBF 자리)
|
|
*/
|
|
(function (global) {
|
|
"use strict";
|
|
const P = (global.STMATE_PLUGINS = global.STMATE_PLUGINS || {
|
|
screens: {},
|
|
actions: {},
|
|
menus: [],
|
|
});
|
|
const UA = global.STMATE_UI_CODE_AMT;
|
|
const UC = global.STMATE_UI_CODE;
|
|
const esc = (s) =>
|
|
String(s == null ? "" : s).replace(
|
|
/[&<>"]/g,
|
|
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c],
|
|
);
|
|
const $ = (id) => document.getElementById(id);
|
|
const num = (v) => Number(String(v == null ? "" : v).replace(/,/g, "")) || 0;
|
|
|
|
// 마지막 입력 자리 — 창(모달) 안 입력칸은 빼고 작업 화면 칸만 기억
|
|
let target = null;
|
|
document.addEventListener("focusin", (e) => {
|
|
const el = e.target;
|
|
if (
|
|
/^(INPUT|TEXTAREA)$/.test(el.tagName) &&
|
|
!el.closest("#stmate-modal-window") &&
|
|
el.type !== "checkbox"
|
|
)
|
|
target = el;
|
|
});
|
|
function insertText(s) {
|
|
const el = target;
|
|
if (!el || !document.body.contains(el))
|
|
return (navigator.clipboard && navigator.clipboard.writeText(s), false);
|
|
const a = el.selectionStart ?? el.value.length,
|
|
b = el.selectionEnd ?? el.value.length;
|
|
el.value = el.value.slice(0, a) + s + el.value.slice(b);
|
|
el.selectionStart = el.selectionEnd = a + s.length;
|
|
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
return true;
|
|
}
|
|
const where = () =>
|
|
target && document.body.contains(target)
|
|
? `넣을 자리: ${target.id || target.className || target.tagName}`
|
|
: "넣을 자리 없음 — 클립보드로 복사";
|
|
|
|
// ── 특수문자 ──
|
|
const dec = typeof TextDecoder !== "undefined" ? new TextDecoder("euc-kr") : null;
|
|
function openSpChar(ctx, api) {
|
|
const s = (ctx.uiState.spc = ctx.uiState.spc || { i: 0, close: true, multi: false, buf: "" });
|
|
const chars = dec ? UA.specialChars(s.i, (b) => dec.decode(b)) : [];
|
|
api.openModal(
|
|
"특수문자 [mMinMsg_]",
|
|
`<div class="stm-form" style="display:flex;gap:10px;"><select id="sc-kind" size="14" class="win-select">${UA.SPECIAL.map(([t], i) => `<option value="${i}" ${s.i === i ? "selected" : ""}>${t}</option>`).join("")}</select>
|
|
<div><div style="display:grid;grid-template-columns:repeat(16,26px);gap:2px;max-height:50vh;overflow:auto;">${chars.map((c) => `<button class="win-btn sc-ch" style="padding:2px;">${esc(c)}</button>`).join("")}</div>
|
|
<div class="stm-row"><label>입력될문자</label><input id="sc-buf" class="win-input" value="${esc(s.buf)}" style="width:160px;" /><label><input type="checkbox" id="sc-close" ${s.close ? "checked" : ""} /> 문자 입력후 종료</label><label><input type="checkbox" id="sc-multi" ${s.multi ? "checked" : ""} /> 다중문자 동시입력</label><button class="win-btn primary" id="sc-put">입력</button></div>
|
|
<p class="stm-note">${esc(where())}</p></div></div>`,
|
|
);
|
|
$("sc-kind").onchange = (e) => ((s.i = Number(e.target.value)), openSpChar(ctx, api));
|
|
$("sc-close").onchange = (e) => (s.close = e.target.checked);
|
|
$("sc-multi").onchange = (e) => (s.multi = e.target.checked);
|
|
const put = (t) => {
|
|
insertText(t);
|
|
s.buf = "";
|
|
if (s.close) api.closeModal();
|
|
};
|
|
document
|
|
.querySelectorAll(".sc-ch")
|
|
.forEach(
|
|
(b) =>
|
|
(b.onclick = () =>
|
|
s.multi
|
|
? (($("sc-buf").value += b.textContent), (s.buf = $("sc-buf").value))
|
|
: put(b.textContent)),
|
|
);
|
|
$("sc-put").onclick = () => put($("sc-buf").value);
|
|
}
|
|
|
|
// ── 사용자문자 편집 ──
|
|
const UKEY = "stmate_user_text";
|
|
const loadU = () => {
|
|
try {
|
|
return JSON.parse(global.localStorage.getItem(UKEY) || "[]");
|
|
} catch (_e) {
|
|
return [];
|
|
}
|
|
};
|
|
const saveU = (list) => {
|
|
try {
|
|
global.localStorage.setItem(UKEY, JSON.stringify(list));
|
|
} catch (_e) {
|
|
// 저장 공간 없음 — 이번 실행 동안만
|
|
}
|
|
};
|
|
function openUser(ctx, api) {
|
|
const list = loadU();
|
|
const u = (ctx.uiState.usr = ctx.uiState.usr || { kw: "", sel: 0 });
|
|
const shown = UA.userTextFind(
|
|
list.map((x, i) => Object.assign({ i }, x)),
|
|
u.kw,
|
|
).map((x) => x.i);
|
|
const all = u.kw ? shown : list.map((_x, i) => i);
|
|
api.openModal(
|
|
"사용자문자 편집 [wM_Edit_USER]",
|
|
`<div class="stm-form"><div class="stm-row"><input id="us-kw" class="win-input" value="${esc(u.kw)}" placeholder="검색 명칭" style="width:160px;" />
|
|
<button class="win-btn" id="us-add">항목추가</button><button class="win-btn" id="us-del">항목삭제</button><button class="win-btn" id="us-up">위로 이동</button><button class="win-btn" id="us-dn">아래로이동</button><button class="win-btn" id="us-init">초기화</button><button class="win-btn" id="us-sp">특수문자 불러오기</button></div>
|
|
<div class="grid-scroll" style="max-height:46vh;"><table class="dbgrid"><thead><tr><th></th><th>검 색 명 칭</th><th>입 력 내 용</th><th>적용</th><th></th></tr></thead><tbody>${all
|
|
.map(
|
|
(i) =>
|
|
`<tr class="${u.sel === i ? "boq-sel" : ""}"><td class="center"><input type="radio" name="us-r" class="us-r" data-i="${i}" ${u.sel === i ? "checked" : ""} /></td><td><input class="cell-input us-k" data-i="${i}" value="${esc(list[i].key)}" /></td><td><input class="cell-input us-t" data-i="${i}" value="${esc(list[i].text)}" style="width:320px;" /></td><td class="center"><input type="checkbox" class="us-on" data-i="${i}" ${list[i].on !== false ? "checked" : ""} /></td><td><button class="win-btn us-put" data-i="${i}">넣기</button></td></tr>`,
|
|
)
|
|
.join("")}</tbody></table></div><p class="stm-note">${esc(where())}</p></div>`,
|
|
);
|
|
const re = () => openUser(ctx, api);
|
|
const save = () => (saveU(list), re());
|
|
$("us-kw").onchange = (e) => ((u.kw = e.target.value), re());
|
|
document
|
|
.querySelectorAll(".us-r")
|
|
.forEach((r) => (r.onchange = () => (u.sel = Number(r.getAttribute("data-i")))));
|
|
document
|
|
.querySelectorAll(".us-k")
|
|
.forEach(
|
|
(r) =>
|
|
(r.onchange = () => (
|
|
(list[Number(r.getAttribute("data-i"))].key = r.value),
|
|
saveU(list)
|
|
)),
|
|
);
|
|
document
|
|
.querySelectorAll(".us-t")
|
|
.forEach(
|
|
(r) =>
|
|
(r.onchange = () => (
|
|
(list[Number(r.getAttribute("data-i"))].text = r.value),
|
|
saveU(list)
|
|
)),
|
|
);
|
|
document
|
|
.querySelectorAll(".us-on")
|
|
.forEach(
|
|
(r) =>
|
|
(r.onchange = () => (
|
|
(list[Number(r.getAttribute("data-i"))].on = r.checked),
|
|
saveU(list)
|
|
)),
|
|
);
|
|
document
|
|
.querySelectorAll(".us-put")
|
|
.forEach(
|
|
(b) =>
|
|
(b.onclick = () => (
|
|
insertText(list[Number(b.getAttribute("data-i"))].text || ""),
|
|
api.closeModal()
|
|
)),
|
|
);
|
|
$("us-add").onclick = () => (
|
|
list.splice(u.sel + 1, 0, { key: "", text: "", on: true }),
|
|
(u.sel += 1),
|
|
save()
|
|
);
|
|
$("us-del").onclick = () => list[u.sel] && (list.splice(u.sel, 1), save());
|
|
const mv = (d) => {
|
|
const j = u.sel + d;
|
|
if (!list[u.sel] || j < 0 || j >= list.length) return;
|
|
[list[u.sel], list[j]] = [list[j], list[u.sel]];
|
|
u.sel = j;
|
|
save();
|
|
};
|
|
$("us-up").onclick = () => mv(-1);
|
|
$("us-dn").onclick = () => mv(1);
|
|
$("us-init").onclick = () => confirm("사용자문자 목록을 비움") && (list.splice(0), save());
|
|
$("us-sp").onclick = () => openSpChar(ctx, api);
|
|
}
|
|
|
|
// ── 내역 메모(DES1·DES2) ──
|
|
const SC = () => global.STMATE_UI_BOQ_SCREEN;
|
|
const selRow = (ctx) =>
|
|
(ctx.dbfData.boq_rows || []).find((r) => Number(r._recno) === (ctx.uiState.boqSel || [])[0]);
|
|
function openMemo(ctx, api) {
|
|
const r = selRow(ctx);
|
|
if (!r) return alert("메모를 넣을 내역 행을 고르세요");
|
|
api.openModal(
|
|
`메모 입력 [wInDlgMemo] — ${esc(r.ITNUM || "")} ${esc(r.NAME1 || r.BDCODE1 || "")}`,
|
|
`<div class="stm-form"><textarea id="mm-t" class="win-input" style="width:520px;height:120px;">${esc([r.DES1, r.DES2].filter((x) => String(x || "").trim()).join("\n"))}</textarea>
|
|
<div class="stm-row"><button class="win-btn primary" id="mm-ok">Ok</button><button class="win-btn" id="mm-no">Cancel</button><input type="file" id="mm-file" accept=".txt" style="display:none;" /><button class="win-btn" id="mm-load">Load</button><button class="win-btn" id="mm-clr">Clear</button></div>
|
|
<p class="stm-note">줄 1 = 비고1(DES1) · 줄 2 = 비고2(DES2) — 내역 저장 때 함께 씀</p></div>`,
|
|
);
|
|
$("mm-no").onclick = api.closeModal;
|
|
$("mm-clr").onclick = () => ($("mm-t").value = "");
|
|
$("mm-load").onclick = () => $("mm-file").click();
|
|
$("mm-file").onchange = (e) => {
|
|
const f = e.target.files[0];
|
|
if (f) f.text().then((t) => ($("mm-t").value = t));
|
|
};
|
|
$("mm-ok").onclick = () => {
|
|
const [a, b] = $("mm-t").value.split(/\r?\n/);
|
|
Object.assign(r, { DES1: (a || "").trim(), DES2: (b || "").trim() });
|
|
api.closeModal();
|
|
SC().after(ctx, api, { ok: true }, true);
|
|
};
|
|
}
|
|
|
|
// ── 동일코드 추려보기 ──
|
|
function openSame(ctx, api) {
|
|
const r = selRow(ctx);
|
|
const c = r ? String(r.BDCODE1 || "").trim() : "";
|
|
if (!c || /^(T\d|E\d|C\d|:)$/.test(c)) return alert("단가코드 행을 고르세요");
|
|
const ev = global.STMATE_VIEW_PRJ_BOQ.evaluateFullBoqRows(ctx).filter(
|
|
(x) => String(x.BDCODE1 || "").trim() === c,
|
|
);
|
|
api.openModal(
|
|
`추려보기 [wSameCode] — ${c}`,
|
|
`<div class="stm-form"><p><strong>${ev.length}번 사용중</strong> · Enter/더블클릭 → 내역서의 해당위치로 이동</p><div class="grid-scroll" style="max-height:50vh;"><table class="dbgrid"><thead><tr><th>공종번호</th><th>명칭</th><th>규격</th><th>단위</th><th>수량</th><th>단가</th><th>금액</th><th>노무비</th><th>재료비</th><th>경비</th><th>비고</th></tr></thead><tbody>${ev
|
|
.map(
|
|
(x) =>
|
|
`<tr class="sm-go" data-r="${x._recno}" tabindex="0"><td>${esc(x.ITNUM)}</td><td>${esc(x.NAME1)}</td><td>${esc(x.SIZE1)}</td><td class="center">${esc(x.UNIT1)}</td><td class="num">${esc(x.QTY1)}</td><td class="num">${api.fmt(num(x.TAMT1))}</td><td class="num">${api.fmt(num(x.T_AMT1))}</td><td class="num">${api.fmt(num(x.L_AMT1))}</td><td class="num">${api.fmt(num(x.M_AMT1))}</td><td class="num">${api.fmt(num(x.S_AMT1))}</td><td>${esc(x.BDCODE1)}</td></tr>`,
|
|
)
|
|
.join("")}</tbody></table></div></div>`,
|
|
);
|
|
const go = (tr) => {
|
|
const rec = Number(tr.getAttribute("data-r"));
|
|
Object.assign(ctx.uiState, { boqSel: [rec], boqLevelMin: 0, boqScrollTo: rec });
|
|
api.closeModal();
|
|
api.renderWorkspace();
|
|
};
|
|
document
|
|
.querySelectorAll(".sm-go")
|
|
.forEach(
|
|
(tr) => (
|
|
(tr.ondblclick = () => go(tr)),
|
|
(tr.onkeydown = (e) => e.key === "Enter" && go(tr))
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── 실적단가 → 일식단가 ──
|
|
function openA2W(ctx, api) {
|
|
if (!ctx.dbfData) return alert("열린 프로젝트가 없음");
|
|
const src = ctx.dbfData.costa_rows || [];
|
|
const A = global.STMATE_A2W;
|
|
const w = (ctx.uiState.a2w = ctx.uiState.a2w || {
|
|
sel: {},
|
|
L: 100,
|
|
M: 0,
|
|
own: false,
|
|
start: UC.nextCode(ctx.dbfData.costn_rows, "W"),
|
|
});
|
|
if (!src.length)
|
|
return api.openModal(
|
|
"실적단가를 일식단가로 변환등록 [wM_Conv_A2W]",
|
|
'<p class="stm-warn" style="padding:12px;">이 프로젝트에 실적단가(COSTA.DBF) 자료가 없음</p>',
|
|
);
|
|
const ed = A ? A.normalizeEditor(w.L, w.M, "L") : { L: w.L, M: w.M, E: 100 - w.L - w.M };
|
|
api.openModal(
|
|
"실적단가를 일식단가로 변환등록 [wM_Conv_A2W]",
|
|
`<div class="stm-form"><div class="grid-scroll" style="max-height:36vh;"><table class="dbgrid"><thead><tr><th>선택</th><th>코드</th><th>공종</th><th>명칭</th><th>규격</th><th>단위</th><th>단가</th><th>노무비율</th></tr></thead><tbody>${src
|
|
.map(
|
|
(r, i) =>
|
|
`<tr><td class="center"><input type="checkbox" class="aw-sel" data-i="${i}" ${w.sel[i] ? "checked" : ""} /></td><td>${esc(r.CODE)}</td><td>${esc(r.CODEA || "")}</td><td>${esc(r.NAME)}</td><td>${esc(r.SIZE)}</td><td>${esc(r.UNIT)}</td><td class="num">${api.fmt(num(r.AAMT1))}</td><td class="num">${esc(r.RATE_L || "")}</td></tr>`,
|
|
)
|
|
.join("")}</tbody></table></div>
|
|
<div class="stm-row"><label>비 목 율( 단가 => 노,재,경 ) 노무비율</label><input id="aw-l" type="number" class="win-input" value="${ed.L}" style="width:70px;" /><label>재료비율</label><input id="aw-m" type="number" class="win-input" value="${ed.M}" style="width:70px;" /><span>경 비 율 ${ed.E} %</span>
|
|
<label><input type="checkbox" id="aw-own" ${w.own ? "checked" : ""} /> 【 노무비율ː개별노무비율 】【 나머지ː재료비우선 】</label></div>
|
|
<div class="stm-row"><label>신규 시작코드</label><input id="aw-start" class="win-input" value="${esc(w.start)}" style="width:90px;" /><button class="win-btn primary" id="aw-ok">변환/생성</button></div></div>`,
|
|
);
|
|
document
|
|
.querySelectorAll(".aw-sel")
|
|
.forEach((i) => (i.onchange = () => (w.sel[i.getAttribute("data-i")] = i.checked)));
|
|
$("aw-ok").onclick = () => {
|
|
Object.assign(w, {
|
|
L: num($("aw-l").value),
|
|
M: num($("aw-m").value),
|
|
own: $("aw-own").checked,
|
|
start: $("aw-start").value.trim().toUpperCase(),
|
|
});
|
|
const rows = src.map((r, i) => ({ sel: Boolean(w.sel[i]), rateL: num(r.RATE_L) }));
|
|
const rates = A
|
|
? A.bulkApply(rows, { L: w.L, M: w.M, sender: "L" }, { individual: w.own })
|
|
: rows;
|
|
const pick = src.map((r, i) => Object.assign({}, r, rates[i])).filter((_r, i) => w.sel[i]);
|
|
if (!pick.length) return alert("변환할 항목을 고르세요");
|
|
const taken = new Set(ctx.dbfData.costn_rows.map(UC.codeOf));
|
|
const made = UA.a2w(pick, { start: w.start, taken });
|
|
const env = { d: ctx.dbfData, state: ctx.state, memo: ctx.uiState.editedSmtMap };
|
|
made.forEach((m) =>
|
|
UC.addCode(env, {
|
|
kind: "W",
|
|
code: m.CODE,
|
|
fields: {
|
|
NAME: m.NAME,
|
|
SIZE: m.SIZE,
|
|
UNIT: m.UNIT,
|
|
MAMT1: String(m.M),
|
|
LAMT1: String(m.L),
|
|
SAMT1: String(m.S),
|
|
TAMT1: String(m.T),
|
|
},
|
|
}),
|
|
);
|
|
ctx.uiState.danMsg = `일식단가(W) ${made.length}건 생성 ${made[0].CODE}~`;
|
|
w.sel = {};
|
|
api.closeModal();
|
|
api.cascadeAndRender();
|
|
};
|
|
}
|
|
|
|
Object.assign(P.actions, {
|
|
Act_UI_SpChar: (ctx, api) => openSpChar(ctx, api),
|
|
Act_UI_UserText: (ctx, api) => openUser(ctx, api),
|
|
Act_UI_A2W: (ctx, api) => openA2W(ctx, api),
|
|
});
|
|
P.menus.push(
|
|
{ menu: "m4", act: "Act_UI_SpChar", label: "특수문자" },
|
|
{ menu: "m4", act: "Act_UI_UserText", label: "사용자문자 편집" },
|
|
{ menu: "m2", act: "Act_UI_A2W", label: "실적단가(A) ⇒ 일식단가(W) 변환" },
|
|
);
|
|
global.STMATE_UI_TEXT = { insertText, openSpChar, openUser, openMemo, openSame, openA2W };
|
|
})(window);
|