- 확인 창 표 가름(`tableOf`): 전국 대상도 자재 표(코드 칸 「전국」) · 미리 셈 문서엔 안 넣음(키 · 단가 없음) - 변형 차례: 「전지역」 낱말 든 지역 품목이 접힌 목록 앞에 오던 것 → (전국) 먼저는 지역 없는 품목에만 - 시험: 화면 도우미 전국 표 · 미리 셈 없음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
322 lines
14 KiB
JavaScript
322 lines
14 KiB
JavaScript
/* M02 일위대가 실무자료 확인 창(338-3) 헬퍼 — `Hopyo_Check` · `Hopyo_Choose` · `Hopyo_Ledger` · `Hopyo_Sheet`
|
|
* 도우미와 `Hopyo_Api` 길을 트랜스파일해 Node 로 돌림 · fetch 대역이 부른 길 · 몸을 모음 · sessionStorage 대역 ·
|
|
* stdin = {lines, rows, ledger} · stdout = {check, choose, ledger, sheet, calls, pick} JSON. */
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
|
|
|
const ROOT = path.join(__dirname, "..", "..");
|
|
const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api" } };
|
|
// DOM · 모듈 메타를 쓰는 창은 대역(도우미 · 길만 시험)
|
|
const STUB_FILES = [
|
|
"M02_MasterTemplete_Labor_Pick",
|
|
"M02_Material_Pick",
|
|
"M02_MasterTemplete_Labor_Api",
|
|
"M02_MasterTemplete_Labor_Input",
|
|
"M01_MasterData_UI_LogicLab_Source",
|
|
"M02_MasterTemplete_Hopyo_CheckRows", // 표 그리기(DOM · Labor_Rows) — 359-3
|
|
];
|
|
const opened = [];
|
|
const stub = new Proxy(
|
|
{},
|
|
{
|
|
get: (_t, k) =>
|
|
k === "currentLanguageIndex"
|
|
? () => 0
|
|
: k === "openModal"
|
|
? (o) => opened.push(o.title)
|
|
: () => {},
|
|
},
|
|
);
|
|
const cache = new Map();
|
|
function loadTs(file) {
|
|
const full = path.resolve(file);
|
|
if (cache.has(full)) return cache.get(full);
|
|
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
|
}).outputText;
|
|
const box = { exports: {} };
|
|
cache.set(full, box.exports);
|
|
const localRequire = (request) => {
|
|
if (STUBS[request]) return STUBS[request];
|
|
if (STUB_FILES.some((name) => request.endsWith(name))) return stub;
|
|
if (request.startsWith(".") && !request.endsWith(".css"))
|
|
return loadTs(`${path.resolve(path.dirname(full), request)}.ts`);
|
|
return stub;
|
|
};
|
|
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
|
return box.exports;
|
|
}
|
|
|
|
const store = new Map();
|
|
global.sessionStorage = {
|
|
getItem: (k) => (store.has(k) ? store.get(k) : null),
|
|
setItem: (k, v) => store.set(k, String(v)),
|
|
removeItem: (k) => store.delete(k),
|
|
};
|
|
|
|
let out0 = null;
|
|
(async () => {
|
|
const input = JSON.parse(fs.readFileSync(0, "utf8"));
|
|
const calls = [];
|
|
global.fetch = async (url, init = {}) => {
|
|
calls.push({
|
|
url,
|
|
method: init.method ?? "GET",
|
|
body: init.body ? JSON.parse(init.body) : null,
|
|
});
|
|
const body = url.includes("/hopyo-picks") || url.includes("/hopyo-ledger") ? { 줄: [] } : {};
|
|
return { status: 200, ok: true, json: async () => body };
|
|
};
|
|
const M02 = path.join(ROOT, "M02_MasterTemplete");
|
|
const check = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Check.ts"));
|
|
const choose = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Choose.ts"));
|
|
const ledger = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Ledger.ts"));
|
|
const sheet = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Sheet.ts"));
|
|
const api = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Api.ts"));
|
|
const pick = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Pick.ts"));
|
|
const draft = loadTs(path.join(M02, "logic", "M02_MasterTemplete_Hopyo_Draft.ts"));
|
|
if (input.drafts) {
|
|
// 359-3 — 실물 줄 → 미리 셈 문서 · 표(서버 calc 에 그대로 보냄)
|
|
const d = check.loadDraft("drafts");
|
|
out0 = input.drafts.map((l) => {
|
|
const parts = check.partsOf(l, d);
|
|
return { 차례: l.차례, table: draft.tableOf(l, parts), doc: draft.draftDoc(l, parts) };
|
|
});
|
|
process.stdout.write(JSON.stringify(out0));
|
|
return;
|
|
}
|
|
if (input.real) {
|
|
// 실물 맞춤 응답(338-4) — 서버 줄을 그대로 먹여 체크 · 몸 · 빠질 줄 · 품 비교 · 글
|
|
const lines = input.real;
|
|
const d = check.loadDraft("real");
|
|
out0 = {
|
|
body: check.bodyOf(lines, d),
|
|
leftOut: check.leftOut(lines, d),
|
|
sure: check.sureLines(lines, d).map((l) => l.차례),
|
|
lines: lines.map((l) => {
|
|
const p = check.partsOf(l, d)[0];
|
|
return {
|
|
이름: l.이름,
|
|
상태: l.맞춤.상태,
|
|
checkable: check.checkable(l, d),
|
|
checked: check.isChecked(l, d),
|
|
compare: check.compareText(l.맞춤.품),
|
|
qty: choose.qtyText(l.맞춤.품?.엑셀),
|
|
target: choose.targetText(p),
|
|
inputs: p ? choose.inputsText(p.입력값 ?? {}, p.못읽음 ?? [], p.출처 ?? {}) : "",
|
|
cands: (l.맞춤.후보 ?? []).map((c) => [
|
|
choose.targetText(c),
|
|
c.판정 ?? "",
|
|
choose.basisTag(c.근거),
|
|
]),
|
|
};
|
|
}),
|
|
};
|
|
process.stdout.write(JSON.stringify(out0));
|
|
return;
|
|
}
|
|
const { lines } = input;
|
|
const scope = { layer: "personal", owner: null };
|
|
const key = check.draftKey(scope, "HP-000001", 3);
|
|
|
|
// 서버 제안 그대로 → 사용자가 만짐(체크 끔 · 뺌 · 고른 대상) → 캐시 다시 읽음
|
|
const fresh = check.loadDraft(key);
|
|
const suggested = check.bodyOf(lines, fresh);
|
|
const d = check.loadDraft(key);
|
|
d.체크[1] = false; // 맞음 줄 체크 끔
|
|
d.뺌[3] = "우리에 없음";
|
|
d.체크[3] = true;
|
|
d.대상[4] = [{ 로직: "GF000211", 이름: "고임돌", 입력값: { 유형: "보통" }, 배: 0.1, 몫: 0.1 }];
|
|
d.체크[4] = true;
|
|
check.saveDraft(key, d);
|
|
const again = check.loadDraft(key);
|
|
const ask = check.newest();
|
|
const a = ask(1);
|
|
const b = ask(1);
|
|
const c = ask(2);
|
|
const out = {
|
|
check: {
|
|
key,
|
|
suggested,
|
|
edited: check.bodyOf(lines, again),
|
|
leftOut: check.leftOut(lines, again),
|
|
checkable: lines.map((l) => check.checkable(l, fresh)),
|
|
sure: check.sureLines(lines, fresh).map((l) => l.차례),
|
|
compare: check.compareText(lines[1].맞춤.품),
|
|
compareMany: check.compareText(lines[3].맞춤.품),
|
|
toTarget: [
|
|
check.toTarget({ 호표: 46, 이름: "고화매트", 입력값: {}, 배: 2.3 }),
|
|
check.toTarget({ 자재: "MT1", 이름: "레미콘", 입력값: {}, 배: 1, 몫: null }),
|
|
check.toTarget({ 전국: true, 이름: "레미콘(전국)-관급", 입력값: {}, 배: 1, 몫: null }),
|
|
],
|
|
newest: [a(), b(), c()],
|
|
dropped: (check.dropDraft(key), store.has(key)),
|
|
},
|
|
choose: {
|
|
hint: [
|
|
choose.searchHint({
|
|
이름: "레미콘타설(장비사용)",
|
|
근거: { 출처: [["건설품셈", "6-1-1"]], 직종: [], 행: [1, 1] },
|
|
}),
|
|
choose.searchHint({ 이름: "(소)고임돌 채집 및 운반" }),
|
|
],
|
|
picked: choose.pickedTargets([
|
|
{ c: { 로직: "GC000206" }, 몫: null },
|
|
{ c: { 로직: "GF000211" }, 몫: 0.1 },
|
|
]),
|
|
inputs: choose.inputsText({ 소량감률: 50, 지역: "서울" }, ["측압"], {
|
|
소량감률: "블록",
|
|
지역: "자동값",
|
|
}),
|
|
word: [choose.builtWord({ 다시씀: false }), choose.builtWord({ 다시씀: true })],
|
|
qty: choose.qtyText(lines[1].맞춤.품.엑셀),
|
|
target: choose.targetText({ 호표: 46, 단위: "㎡" }),
|
|
// 358-1 후보 묶음 — 1위 / 출처 같음 / 이름 닮은 로직 / 숫자만 닮음(약한 후보 끝) · 같은 이름 책
|
|
groups: (() => {
|
|
const m = {
|
|
후보: [
|
|
{ 로직: "A", 이름: "되메우기", 판정: "맞음", 뜻겹침: true, 책: "산림품셈" },
|
|
{ 로직: "B", 이름: "야면석 채집", 판정: "후보", 뜻겹침: false },
|
|
{
|
|
로직: "C",
|
|
이름: "되메우기",
|
|
판정: "후보",
|
|
뜻겹침: true,
|
|
출처같음: true,
|
|
책: "건설품셈",
|
|
},
|
|
{ 로직: "D", 이름: "표토펴기", 판정: "다른 점", 뜻겹침: false },
|
|
],
|
|
이름후보: [
|
|
{ 로직: "A", 이름: "되메우기" },
|
|
{ 로직: "E", 이름: "되메우기(노상재)" },
|
|
],
|
|
};
|
|
const g = choose.candGroups(m);
|
|
const twins = choose.sameNames(g);
|
|
return {
|
|
groups: g.map((x) => [x.제목, x.접힘, x.후보.map((c) => c.로직)]),
|
|
text: g.flatMap((x) => x.후보).map((c) => choose.candText(c, twins)),
|
|
material: choose
|
|
.candGroups({ 후보: [{ 자재: "M1" }, { 자재: "M2" }] }, true)
|
|
.map((x) => x.제목),
|
|
// 361-1 지역만 다른 자재 — 1위 전국 · 지역 변형 접힘 · 고르면 그 지역 · 전국 되돌림
|
|
national: (() => {
|
|
const all = [
|
|
{ 전국: true, 이름: "레미콘(전국)-관급", 규격: "25-21-120", 단위: "㎥" },
|
|
{ 자재: "M1", 이름: "레미콘(서울)-사급", 변형: true },
|
|
{ 자재: "M2", 이름: "레미콘(관급)-강원도(강릉시)", 변형: true },
|
|
];
|
|
const g = choose.candGroups({ 후보: all }, true);
|
|
const id = (c) => c.자재 ?? (c.전국 ? "전국" : "");
|
|
return {
|
|
groups: g.map((x) => [x.제목, x.접힘, x.후보.map(id)]),
|
|
text: choose.candText(all[0], new Set()),
|
|
region: choose.pickedTargets([{ c: all[2], 몫: null }]),
|
|
back: choose.pickedTargets([{ c: all[0], 몫: null }]),
|
|
hit: choose
|
|
.candGroups({ 후보: [all[2], all[0], all[1]] }, true)
|
|
.map((x) => [x.제목, x.후보.map(id)]),
|
|
};
|
|
})(),
|
|
pick: [
|
|
choose.canPickMaterial({ 갈래: "단가산출", 단가: { 재료비: 10 } }),
|
|
choose.canPickMaterial({ 갈래: "단가산출", 단가: { 재료비: null } }),
|
|
choose.canPickMaterial({ 갈래: "별도계상", 단가: {} }),
|
|
],
|
|
};
|
|
})(),
|
|
},
|
|
ledger: (() => {
|
|
const rows = input.ledger;
|
|
const edits = {
|
|
"가|b|m3": null, // 지움
|
|
"나||m3": { 원래: { 품명: "나", 규격: "", 단위: "m3" }, 갈래: "안 씀", 짝: [] },
|
|
"다||개": ledger.entryOf(rows[2]), // 위 층 → 내 층
|
|
};
|
|
const merged = ledger.mergeRows(rows, edits, { layer: "personal", owner: "7/42" });
|
|
return {
|
|
key: ledger.ledgerKey({ layer: "company", owner: "7" }),
|
|
merged: merged.map((r) => [r.열쇠, r.층, r.고침, !!r.고친, r.갈래]),
|
|
filter: ledger.filterRows(merged, "MT1", "").map((r) => r.열쇠),
|
|
kind: ledger.filterRows(merged, "", "안 씀").map((r) => r.열쇠),
|
|
pair: ledger.pairText({ 로직: "GC000198", 입력값: { 소량감률: 50 }, 배: 1, 몫: 0.1 }),
|
|
entry: ledger.entryOf({ ...rows[0], 갈래: "우리에 없음" }),
|
|
};
|
|
})(),
|
|
draft: (() => {
|
|
// 359-3 — 미리 셈 문서(우리 수량 = 엑셀 × 배 · 별도계상 · 같은 자재 더함 · 같은 로직 둘째 · 호표 · 배 없음 뺌)
|
|
const L = { 로직: "GC000198", 입력값: { 소량감률: 50 }, 배: 2 };
|
|
const M = { 자재: "MT1", 입력값: {}, 배: 1 };
|
|
return {
|
|
logic: draft.draftDoc({ 수량: 0.5, 갈래: "단가산출" }, [
|
|
L,
|
|
{ ...L, 배: 3 },
|
|
{ 호표: 46, 입력값: {}, 배: 1 },
|
|
{ 로직: "GF000101", 입력값: {}, 배: null },
|
|
]),
|
|
mat: draft.draftDoc({ 수량: 2, 갈래: "별도계상" }, [M, { ...M, 배: 0.5 }]),
|
|
direct: draft.draftDoc({ 수량: null, 갈래: "자재" }, [M]).자재[0],
|
|
none: [
|
|
draft.draftDoc({ 수량: 1, 갈래: "단가산출" }, []),
|
|
draft.draftDoc({ 수량: 1, 갈래: "호표" }, [{ 호표: 3, 입력값: {}, 배: 1 }]),
|
|
draft.draftDoc({ 수량: 1, 갈래: "별도계상" }, [{ 전국: true, 입력값: {}, 배: 1 }]),
|
|
],
|
|
table: [
|
|
draft.tableOf({ 갈래: "단가산출" }, [M]),
|
|
draft.tableOf({ 갈래: "자재" }, [L]),
|
|
draft.tableOf({ 갈래: "자재" }, [L, M]),
|
|
draft.tableOf({ 갈래: "별도계상" }, []),
|
|
draft.tableOf({ 갈래: "%" }, []),
|
|
draft.tableOf({ 갈래: "별도계상" }, [{ 전국: true, 입력값: {}, 배: 1 }]),
|
|
],
|
|
};
|
|
})(),
|
|
sheet: {
|
|
order: sheet.buildOrder([
|
|
{ 번: 45, 아래: [46] },
|
|
{ 번: 46, 아래: [] },
|
|
{ 번: 3, 아래: [] },
|
|
{ 번: 7, 아래: [8] },
|
|
{ 번: 8, 아래: [7] },
|
|
]),
|
|
linked: [
|
|
{ 맞춤: [2, 2] },
|
|
{ 맞춤: [1, 2] },
|
|
{ 맞춤: [0, 0] },
|
|
{ 맞춤: [3, 2] },
|
|
{ 맞춤: [1, 4], 연결할: 3, 연결된: 3 }, // 358-3 두 칸이 앞섬(% · 노임 줄 밖)
|
|
{ 맞춤: [3, 3], 연결할: 3, 연결된: 2 },
|
|
{ 맞춤: [0, 1], 연결할: 0, 연결된: 0 },
|
|
].map(sheet.allLinked),
|
|
},
|
|
};
|
|
// 길 — 만들기(줄 · 통째) · 호표 하나(만들 층) · 미리 셈 · 장부
|
|
const from = { 층: "system", 주인: null, 키: "HP-000001", 번: 3 };
|
|
const target = { layer: "company", owner: "7" };
|
|
await api.buildHopyo(target, from, suggested);
|
|
await api.buildHopyo(target, from, [], { 대상: [{ 로직: "GF000225", 배: 1 }] });
|
|
await api.readHopyoOne({ layer: "system", owner: null }, "HP-000001", 3, target);
|
|
await api.guessHopyo(scope, "HP-000001", 3, 4, [
|
|
{ 로직: "GF000211", 입력값: {}, 배: 0.1, 몫: 0.1 },
|
|
]);
|
|
await api.readLedger(target);
|
|
await api.saveLedger(target, { "가|b|m3": null });
|
|
out.calls = calls.splice(0);
|
|
// [로직 추가] 실무 호표 — 확인된 것 = 바로 참조 · 아닌 것 = 확인 창(만들기 길 안 부름)
|
|
const picked = [];
|
|
const lib = {
|
|
layer: "company",
|
|
owner: "7",
|
|
pick: (ref, name, alias) => picked.push({ ref, name, alias }),
|
|
};
|
|
for (const row of input.rows) await pick.pickHopyo(row, lib);
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
out.pick = { picked, calls: calls.splice(0), opened };
|
|
process.stdout.write(JSON.stringify(out));
|
|
})().catch((error) => {
|
|
process.stderr.write(String(error?.stack ?? error));
|
|
process.exit(1);
|
|
});
|