Files
Aislo/tools/est-test/native-code-change-view.js
T
eomsangdonandClaude Opus 5.5 5e6c2988fc feat(est-test): 엑셀형 산출서 편집 동작과 조달청 표준공사코드 적용
- 엑셀형 산출서: 범위 고르기, 행·열 삽입/삭제(수식 참조·병합·수량 연결 이동, 지운 칸 #REF!), 잘라내기·복사·붙여넣기(상대 참조), 내용지우기, 굵게·기울임·밑줄·정렬, 병합·분리, 대각선, 다시 계산, 품명·규격·단위로 지정, 원본 .xls 병합 정보 읽기
- 조달청 표준공사코드 적용(원본 TfrmCodeChange): 7종 탭, 코드변경·검사 색·변경취소·적용(참조 행 포함, 재계산 없음), 코드일괄변경(.estx), 자동변경, 다산코드정리, 사용자정의코드, 검색. 조달청 목록은 사용자가 고른 PPS_*.dat 를 /api/pps-table 로 읽음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 12:44:54 +09:00

281 lines
12 KiB
JavaScript

// 「조달청 표준공사코드 적용」 대화 화면 — 왼쪽 조달청 목록(불러온 PPS 표) · 오른쪽 작업데이타 7종 탭. 계산은 native-code-change.js.
const EstCodeChangeView = (() => {
const PAGE = 100,
COLORS = { 1: "#fff59d", 2: "#f8d7e0", 3: "#ffcc80" };
const C = () => EstCodeChange;
function open(state, tag) {
const view = state.view,
guess = {
HeadMainDat: 103,
CostMainDat: 104,
MachMainDat: 105,
SilJukDat: 21,
EstiMateDat: 101,
StuffcDat: 5,
LaborcDat: 6,
}[view];
const cc = (state.codeChange ||= {
pps: {},
word: "",
rowWord: "",
autoMove: true,
nameChange: false,
});
cc.tag = tag ?? cc.tag ?? guess ?? 6;
cc.rows = C().load(state.project, cc.tag);
C().validate(state.project, cc.tag, cc.rows);
cc.sel = cc.rows.length ? 0 : undefined;
cc.page = 0;
cc.dirty = false;
cc.pick = undefined;
draw(state);
}
function ppsKind(tag) {
return [21, 103, 104].includes(tag) ? "HEAD" : "STUFFC";
}
function shownRows(cc) {
const w = String(cc.rowWord || "").replace(/[\s|()]/g, "");
return cc.rows
.map((r, i) => ({ r, i }))
.filter(
({ r }) =>
!w ||
(cc.rowMode === "name"
? `${r.Name}${r.Size}`.replace(/[\s|()]/g, "").includes(w)
: String(r.OldCode)
.replace(/[\s|()]/g, "")
.includes(w)),
);
}
function draw(state) {
const { esc } = EstApp,
cc = state.codeChange,
C2 = C(),
list = cc.pps[ppsKind(cc.tag)] || [],
found = C2.search(list, cc.word).slice(0, PAGE),
rows = shownRows(cc),
pages = Math.max(1, Math.ceil(rows.length / PAGE)),
page = Math.min(cc.page, pages - 1);
const tabs = C2.ORDER.map(
(t) =>
`<button data-action="code-change-tab" data-tag="${t}"${t === cc.tag ? ' aria-pressed="true" style="font-weight:bold"' : ""}>${C2.NAMES[t]}</button>`,
).join("");
const left = `<div style="flex:1 1 420px;min-width:0"><b>조달청코드 목록</b> (${ppsKind(cc.tag) === "HEAD" ? "단산·일위·표준 = PPS_HEAD" : "노임·자재·장비·견적 = PPS_STUFFC"}) <label>목록 불러오기 <input type="file" accept=".dat" data-cc-pps></label><div><input data-cc-word value="${esc(cc.word)}" placeholder="검색어 (A&B = 둘 다)"><button data-action="code-change-search">검색</button><button data-action="code-change-all">전체보기</button> ${list.length ? `${found.length}/${list.length}` : "목록 없음 — 원본 System\\MainSys 의 PPS_*.dat 를 고르세요"}</div><div style="max-height:360px;overflow:auto"><table class="sheet"><thead><tr><th></th><th>코드</th><th>명칭</th><th>규격</th><th>단위</th></tr></thead><tbody>${found
.map(
(x, i) =>
`<tr><td><input type="radio" name="cc-pick" data-cc-pick="${i}"${cc.pick?.Code === x.Code ? " checked" : ""}></td><td>${esc(x.Code)}</td><td>${esc(x.Name)}</td><td>${esc(x.Size)}</td><td>${esc(x.Unit)}</td></tr>`,
)
.join("")}</tbody></table></div></div>`;
const body = rows
.slice(page * PAGE, page * PAGE + PAGE)
.map(
({ r, i }) =>
`<tr>${[
`<input type="radio" name="cc-row" data-cc-row="${i}"${cc.sel === i ? " checked" : ""}>`,
esc(r.Index ?? ""),
esc(r.OldCode),
`<input data-cc-code="${i}" value="${esc(r.Code)}" size="18"${r.Checked ? ' style="font-weight:bold"' : ""}>`,
esc(r.Name),
esc(r.Size),
esc(r.Unite),
esc(r.Bigo),
]
.map((v) => `<td${r.Flag ? ` style="background:${COLORS[r.Flag]}"` : ""}>${v}</td>`)
.join("")}</tr>`,
)
.join("");
const user = cc.pick ? safeUser(state) : "";
const right = `<div style="flex:1 1 560px;min-width:0"><div>${tabs}</div><div><input data-cc-rowword value="${esc(cc.rowWord || "")}" placeholder="작업데이타 검색"><select data-cc-rowmode><option value="code">코드검색</option><option value="name"${cc.rowMode === "name" ? " selected" : ""}>명칭검색</option></select><button data-action="code-change-rowsearch">검색</button> ${rows.length}행 · <button data-action="code-change-page" data-step="-1"${page ? "" : " disabled"}>이전</button>${page + 1}/${pages}<button data-action="code-change-page" data-step="1"${page + 1 < pages ? "" : " disabled"}>다음</button></div><div style="max-height:330px;overflow:auto"><table class="sheet"><thead><tr><th></th><th>No</th><th>당초코드</th><th>변경코드</th><th>명칭</th><th>규격</th><th>단위</th><th>비고</th></tr></thead><tbody>${body}</tbody></table></div><p class="business-note">*조달청호환 코드로 미변경(노랑), 중복코드(연분홍 비고란 참조), 코드자리수틀림(진황색)*</p></div>`;
const tools = [
`<button data-action="code-change-input">코드변경</button>`,
`<button data-action="code-change-user"${user ? "" : " disabled"}>사용자정의코드 ${EstApp.esc(user)}</button>`,
`<button data-action="code-change-cancel">변경취소</button>`,
`<button data-action="code-change-auto"${cc.tag === 21 ? "" : " disabled"}>자동변경</button>`,
`<button data-action="code-change-bulk">코드일괄변경(.estx 선택)</button>`,
`<button data-action="code-change-dasan">다산코드정리</button>`,
`<button data-action="code-change-apply"${cc.dirty ? "" : " disabled"}>적용</button>`,
`<button data-action="code-change-close">닫기</button>`,
`<label><input type="checkbox" data-cc-opt="autoMove"${cc.autoMove ? " checked" : ""}> 코드변경후 다음항목</label>`,
`<label><input type="checkbox" data-cc-opt="nameChange"${cc.nameChange ? " checked" : ""}> 명칭,규격 변경</label>`,
].join(" ");
EstApp.showDialog(
`조달청 표준공사코드 적용 — ${C2.NAMES[cc.tag]}`,
`${cc.message ? `<p class="business-note" role="status">${esc(cc.message)}</p>` : ""}<div style="display:flex;flex-wrap:wrap;gap:12px">${left}${right}</div><div class="business-tools">${tools}</div>`,
);
cc.message = "";
}
function safeUser(state) {
const cc = state.codeChange;
if (!cc.pick || [21, 105, 101].includes(cc.tag)) return "";
try {
return C().userCode(state.project, cc.tag, cc.rows, cc.pick.Code);
} catch {
return "";
}
}
async function run(name, state, node) {
const cc = state.codeChange;
if (name === "code-change") return open(state);
if (!cc) return;
const C2 = C();
if (name === "code-change-tab") {
if (cc.dirty && confirm(`변경된 ${C2.NAMES[cc.tag]} 의 코드를 내역서 전체에 적용할까요?`))
applyNow(state);
return open(state, Number(node.dataset.tag));
}
if (name === "code-change-search")
cc.word = document.querySelector("[data-cc-word]")?.value ?? "";
else if (name === "code-change-all") cc.word = "";
else if (name === "code-change-rowsearch") {
cc.rowWord = document.querySelector("[data-cc-rowword]")?.value ?? "";
cc.rowMode = document.querySelector("[data-cc-rowmode]")?.value;
cc.page = 0;
} else if (name === "code-change-page")
cc.page = Math.max(0, cc.page + Number(node.dataset.step));
else if (name === "code-change-input" || name === "code-change-user") {
if (!cc.pick) throw Error("조달청코드 목록에서 코드를 고르세요.");
const code = name === "code-change-user" ? safeUser(state) : cc.pick.Code;
C2.input(
state.project,
cc.tag,
cc.rows,
cc.sel,
code,
name === "code-change-input" && cc.nameChange ? { ...cc.pick, nameChange: true } : null,
);
cc.dirty = true;
if (cc.autoMove && cc.sel < cc.rows.length - 1) cc.sel++;
} else if (name === "code-change-cancel") C2.cancel(cc.rows, cc.sel);
else if (name === "code-change-auto") {
C2.auto(cc.rows);
cc.dirty = true;
} else if (name === "code-change-dasan") {
if (!confirm("코드를 일련번호로 새로 부여하시겠습니까?")) return;
C2.dasan(cc.rows, cc.tag);
cc.dirty = true;
} else if (name === "code-change-bulk") return pickEstx(state);
else if (name === "code-change-apply") {
if (!confirm(`변경된 ${C2.NAMES[cc.tag]} 의 코드를 내역서 전체에 적용할까요?`)) return;
applyNow(state);
} else if (name === "code-change-close") {
if (
cc.dirty &&
!confirm("내역서에 변경된 코드를 적용하지 않았습니다.코드변경작업을 종료하시겠습니까?")
)
return;
EstApp.byId("dialog").close();
state.codeChange = { ...cc, rows: [], dirty: false };
return;
}
draw(state);
}
function applyNow(state) {
const cc = state.codeChange,
C2 = C();
if (
C2.validate(state.project, cc.tag, cc.rows) &&
!confirm("조달청코드로 변경안된 데이타가 있습니다.그래도 적용하시겠습니까??")
)
return;
let count = 0;
EstApp.change((project) => (count = C2.apply(project, cc.tag, cc.rows)));
cc.dirty = false;
state.notice = `${C2.NAMES[cc.tag]}의 코드변경이 완료 되었습니다. (${count}행)`;
}
function pickEstx(state) {
const cc = state.codeChange,
picker = document.createElement("input");
picker.type = "file";
picker.accept = ".estx";
picker.addEventListener("change", async () => {
try {
const file = picker.files[0];
if (!file) return;
if (
!confirm(
`현재 작업데이타의 ${C().NAMES[cc.tag]} 코드를 "${file.name}"의 코드로 변경하시겠습니까?`,
)
)
return;
const source = await EstApp.api(`/api/import?name=${encodeURIComponent(file.name)}`, {
method: "POST",
body: file,
});
const n = C().bulk(state.project, cc.tag, cc.rows, source);
if (n) cc.dirty = true;
cc.message = n
? `${n}개 코드를 바꿨습니다. 적용을 누르세요.`
: "코드가 변경된 항목이 없습니다.";
} catch (error) {
cc.message = error.message;
}
draw(state);
});
picker.click();
}
function install() {
document.addEventListener("change", async (event) => {
const t = event.target,
state = EstApp.state,
cc = state.codeChange;
if (!cc?.rows) return;
try {
if (t.dataset.ccRow !== undefined) cc.sel = Number(t.dataset.ccRow);
else if (t.dataset.ccPick !== undefined)
cc.pick = EstCodeChange.search(cc.pps[ppsKind(cc.tag)] || [], cc.word)[
Number(t.dataset.ccPick)
];
else if (t.dataset.ccOpt !== undefined) cc[t.dataset.ccOpt] = t.checked;
else if (t.dataset.ccCode !== undefined) {
const i = Number(t.dataset.ccCode);
C().input(state.project, cc.tag, cc.rows, i, t.value);
cc.dirty = true;
} else if (t.dataset.ccPps !== undefined) {
const file = t.files[0];
if (!file) return;
const rows = await EstApp.api("/api/pps-table", { method: "POST", body: file });
const kind = rows.some((r) => /공사$/.test(r.Gubun ?? "")) ? "HEAD" : "STUFFC";
cc.pps[kind] = rows;
cc.message = `조달청 ${kind === "HEAD" ? "공종" : "물품"} 코드 ${rows.length}개를 불러왔습니다.`;
} else return;
} catch (error) {
cc.message = error.message;
}
draw(state);
});
}
const names = new Set([
"code-change",
"code-change-tab",
"code-change-search",
"code-change-all",
"code-change-rowsearch",
"code-change-page",
"code-change-input",
"code-change-user",
"code-change-cancel",
"code-change-auto",
"code-change-dasan",
"code-change-bulk",
"code-change-apply",
"code-change-close",
]);
function handle(name, state, node) {
if (!names.has(name)) return false;
run(name, state, node).catch((error) => {
if (!state.codeChange?.rows) return void (state.notice = error.message);
state.codeChange.message = error.message;
draw(state);
});
return true;
}
if (typeof document !== "undefined" && document.addEventListener) install();
return { open, handle };
})();