Files
Aislo/tools/est-test/native-code-change.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

274 lines
9.4 KiB
JavaScript

// 원본 「조달청 표준공사코드 적용」(mnuCodeChange 0x10d10fc → TfrmCodeChange — 디스어셈블 근거 tmp 명세 spec_codechange).
// 작업데이타 7종(일위 103·단산 104·기계 105·표준 21·견적 101·자재 5·노임 6)의 코드를 메모리 사본에서 바꾼 뒤 「적용」 때
// 자기 표 Code, 본문 머리 Code·TitleCode, 참조 행(LinkIndex = 종류 · Index ≤ 0 · Code = 옛 코드) Code 만 바꾼다(재계산 없음).
// 검사(df5e34): EST 코드 = 노랑(1) · 다른 표에 같은 코드 = 연분홍(2, 비고) · 자리수(일위·단산·표준 12, 장비 17, 견적·자재·노임 16) = 진황(3).
// 코드일괄변경(df2b98): 고른 .estx 의 같은 종류 표에서 품명·규격·단위 맞춤(자동연결 규칙) 첫 행 — 현재 EST 코드 → 비EST 코드만,
// 표준(21)은 「.」 뺀 옛 코드를 부분일치로. 자동변경(표준 탭): 행 번호 2~2092 C · 2095~2717 A · 2720~3361 M 앞에, 끝에 0, 「.」 제거.
// 다산코드정리: 종류 글자 + 10자리 일련번호. 사용자정의코드(df4b64): 고른 코드 앞자리 + Z 꼴 빈 번호.
// 원본의 조달청 목록(PPS_STUFFC·PPS_HEAD)은 설치 폴더 System\MainSys 의 .dat 를 사용자가 골라 불러온다(내장 자료 없음).
const EstCodeChange = (() => {
const TABLES = {
103: "HeadTitleDat",
104: "CostTitleDat",
105: "MachTitleDat",
21: "SilJukDat",
101: "EstiMateDat",
5: "StuffcDat",
6: "LaborcDat",
};
const MAIN = { 103: "HeadMainDat", 104: "CostMainDat", 105: "MachMainDat" };
const NAMES = {
103: "일위대가",
104: "단가산출",
105: "기계경비",
21: "표준시장단가",
101: "견적단가",
5: "자재단가",
6: "노임단가",
};
const SHORT = {
103: "일위",
104: "단가",
105: "장비",
21: "표준",
101: "견적",
5: "자재",
6: "노임",
};
const LETTER = {
5: "M",
6: "L",
15: "N",
21: "S",
101: "W",
103: "U",
104: "D",
105: "E",
151: "G",
};
const ORDER = [103, 104, 105, 21, 101, 5, 6];
const num = (v) => Number(v) || 0;
const trim = (v) => String(v ?? "").trim();
const norm = (s) => String(s ?? "").replace(/[\s|()]/g, "");
const letter = (tag) => LETTER[tag] || "Z";
const isEst = (code, tag) => {
const c = trim(code);
return c.length > 0 && c.length <= 11 && c[0] === letter(tag) && c[1] === "0";
};
function load(project, tag) {
return (project.tables[TABLES[tag]] || []).map((row) => ({
Index: row.Index,
OldCode: row.Code ?? "",
Code: row.Code ?? "",
Name: row.Name ?? "",
Size: row.Size ?? "",
Unite: row.Unite ?? "",
Flag: 0,
Checked: false,
Bigo: "",
}));
}
// 다른 6 표에 같은 코드가 있으면 그 행 설명
function elsewhere(project, tag, code) {
for (const t of ORDER) {
if (t === tag) continue;
const row = (project.tables[TABLES[t]] || []).find((r) => trim(r.Code) === code);
if (row) return `${SHORT[t]}"${row.Index ?? ""}.${row.Name ?? ""}(${row.Size ?? ""})" `;
}
return "";
}
function check(project, tag, row, code = trim(row.Code)) {
row.Flag = 0;
if (isEst(code, tag)) return (row.Flag = 1);
const other = elsewhere(project, tag, code);
if (other) {
row.Bigo = other;
return (row.Flag = 2);
}
let size = [21, 103, 104].includes(tag) ? 12 : tag === 105 ? 17 : 16;
if (tag === 105 && !/^[0-9Z]/.test(code)) return (row.Flag = 1);
if (code.length !== size) row.Flag = 3;
return row.Flag;
}
function validate(project, tag, rows) {
for (const row of rows) {
row.Flag = 0;
row.Checked = false;
row.Bigo = "";
if (trim(row.Code)) check(project, tag, row);
}
return rows.filter((r) => r.Flag).length;
}
const MESSAGE = {
1: "조달청 코드가 아닙니다.",
3: "코드자리수가 틀립니다.일위,단산,표준:12자리, 장비:17자리 ,견적,노임,자재:16자리",
};
// 코드변경(df20f4): 목록 코드 또는 직접 입력을 현재 행에. 실패하면 문구를 던진다.
function input(project, tag, rows, i, code, from) {
let c = trim(code);
if (tag === 105 && c.length === 16) c += "0";
if (!rows[i]) throw Error("바꿀 작업데이타 행을 고르세요.");
const dup = rows.findIndex((r, j) => j !== i && trim(r.Code) === c);
if (dup >= 0) {
rows[dup].Flag = 2;
throw Error("같은 코드가 있습니다. 확인하세요.");
}
const probe = { ...rows[i] };
const flag = check(project, tag, probe, c);
if (flag === 2) throw Error(`${probe.Bigo}에 같은 코드가 있습니다.`);
if (flag) throw Error(MESSAGE[flag]);
rows[i].Code = c;
if (from?.nameChange && trim(from.Name)) {
rows[i].Name = from.Name;
rows[i].Size = from.Size ?? "";
rows[i].Unite = from.Unit ?? from.Unite ?? "";
}
}
function cancel(rows, i) {
if (rows[i]) rows[i].Code = rows[i].OldCode;
}
// 적용(df1c40)
function apply(project, tag, rows) {
const t = project.tables,
own = t[TABLES[tag]] || [];
let changed = 0;
for (const r of rows) {
const neu = trim(r.Code),
old = trim(r.OldCode);
if (!neu || neu === old) continue;
const same = (x) => (x.Name ?? "") === r.Name && (x.Size ?? "") === r.Size;
let hit = own.find((x) => trim(x.Code) === old);
if (hit && !same(hit)) hit = own.find((x) => trim(x.Code) === old && same(x));
if (hit) {
hit.Code = neu;
changed++;
}
if (MAIN[tag])
for (const x of t[MAIN[tag]] || [])
if (trim(x.TitleCode) === old) {
x.Code = neu;
x.TitleCode = neu;
}
const refs =
tag === 103
? ["HeadMainDat", "CostMainDat"]
: tag === 104
? ["CostMainDat", "HeadMainDat"]
: tag === 105
? ["MachMainDat", "CostMainDat", "HeadMainDat"]
: ["MachMainDat", "CostMainDat", "HeadMainDat"];
for (const name of [...refs, "EstMainDat"])
for (const x of t[name] || [])
if (num(x.LinkIndex) === tag && trim(x.Code) === old && num(x.Index) <= 0) {
x.Code = neu;
changed++;
}
r.OldCode = neu;
}
return changed;
}
// 코드일괄변경(df2b98) — source = 고른 .estx 를 읽은 공사
function bulk(project, tag, rows, source) {
const list = source.tables?.[TABLES[tag]] || [];
let changed = 0;
rows.forEach((r) => (r.Checked = false));
for (const r of rows) {
const cur = trim(r.Code);
if (!cur) continue;
let found;
if (tag === 21) {
if (!cur.includes(".")) continue;
const k = cur.replaceAll(".", "");
found = list.find((x) => String(x.Code ?? "").includes(k));
if (!found) continue;
const c = trim(found.Code);
if (rows.some((x) => trim(x.Code) === c) || elsewhere(project, tag, c)) continue;
r.Code = c;
} else {
found = EstAutoLink.find(source, tag, EstAutoLink.key(r, 0));
if (!found) continue;
const c = trim(found.Code);
if (rows.some((x) => trim(x.Code) === c) || elsewhere(project, tag, c)) continue;
if (!isEst(cur, tag) || isEst(c, tag)) continue;
r.Code = c;
r.Flag = 0;
}
r.Checked = true;
changed++;
}
return changed;
}
function auto(rows) {
rows.forEach((r, i) => {
const n = i + 1,
c = trim(r.Code);
if (!c) return;
const head =
n >= 2 && n <= 2092
? "C"
: n >= 2095 && n <= 2717
? "A"
: n >= 2720 && n <= 3361
? "M"
: "";
r.Code = (head ? `${head}${c}0` : c).replaceAll(".", "");
});
}
function dasan(rows, tag) {
rows.forEach((r, i) => (r.Code = letter(tag) + String(i).padStart(10, "0")));
}
function userCode(project, tag, rows, base) {
const s = `${trim(base)}00000000`;
const make = (n) => {
const nn = `000${n}`;
if (tag === 103 || tag === 104) return `${s.slice(0, 8)}Z${nn.slice(-3)}`;
if (tag === 105) return `${s.slice(0, 8)}000Z${nn.slice(-4)}0`;
if (tag === 101 || tag === 5) return `${s.slice(0, 6)}ZZ000Z${nn.slice(-4)}`;
if (tag === 6) return `${s.slice(0, 8)}010Z${nn.slice(-4)}`;
return "";
};
const extra = { 103: "CostTitleDat", 104: "HeadTitleDat", 105: "MachTitleDat" }[tag];
for (let n = 0; n <= 999; n++) {
const c = make(n);
if (!c) return "";
if (rows.some((r) => trim(r.Code) === c)) continue;
if (extra && (project.tables[extra] || []).some((r) => trim(r.Code) === c)) continue;
return c;
}
throw Error("사용자 정의코드 갯수범위를 초과 했습니다.");
}
// 목록 검색(df6b5c): 정규화한 품명+규격에 검색어 포함, 「&」 이면 두 조각 모두
function search(list, word) {
const w = norm(word);
if (!w) return list;
const parts = w.includes("&") ? w.split("&").slice(0, 2) : [w];
return list.filter((x) => {
const k = norm(x.Name) + norm(x.Size);
return parts.every((p) => k.includes(p));
});
}
return {
TABLES,
NAMES,
ORDER,
isEst,
load,
validate,
input,
cancel,
apply,
bulk,
auto,
dasan,
userCode,
search,
};
})();
if (typeof module !== "undefined" && module.exports) module.exports = EstCodeChange;