Files
Aislo/resources/tester/helper_m02_hopyo_check.cjs
T
eomsangdonandClaude Opus 5.5 3abb10cc13 fix(M02 실무자료): 361-1 전국 자재 = 확인 창 자재 표 · 지역 변형 차례 = (전국) 낱말은 지역 없는 품목에만
- 확인 창 표 가름(`tableOf`): 전국 대상도 자재 표(코드 칸 「전국」) · 미리 셈 문서엔 안 넣음(키 · 단가 없음)
- 변형 차례: 「전지역」 낱말 든 지역 품목이 접힌 목록 앞에 오던 것 → (전국) 먼저는 지역 없는 품목에만
- 시험: 화면 도우미 전국 표 · 미리 셈 없음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
2026-10-16 23:23:11 +09:00

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);
});