- 엑셀형 산출서: 범위 고르기, 행·열 삽입/삭제(수식 참조·병합·수량 연결 이동, 지운 칸 #REF!), 잘라내기·복사·붙여넣기(상대 참조), 내용지우기, 굵게·기울임·밑줄·정렬, 병합·분리, 대각선, 다시 계산, 품명·규격·단위로 지정, 원본 .xls 병합 정보 읽기 - 조달청 표준공사코드 적용(원본 TfrmCodeChange): 7종 탭, 코드변경·검사 색·변경취소·적용(참조 행 포함, 재계산 없음), 코드일괄변경(.estx), 자동변경, 다산코드정리, 사용자정의코드, 검색. 조달청 목록은 사용자가 고른 PPS_*.dat 를 /api/pps-table 로 읽음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
274 lines
9.4 KiB
JavaScript
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;
|