/* 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 { makeLoader } = require("./helper_ts_stub_loader.cjs"); 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 { loadTs } = makeLoader({ root: ROOT, requireFor: (full) => (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; }, }); 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); });