/* 항목 고르기 모달 자료 창구 시험 헬퍼(계획서 25-2d) — `ui_template_item_picker_project.ts` 와 * 선택 입력 카드 셈(`M02_MasterTemplete_Inputs_Model.ts`)을 트랜스파일해 가짜 서버(fetch 대역) 위에서 돌린다. * 서버 모양은 sub6 복사본 길(`M02_Project_Items_Router.py`) · sub4 입력 길 그대로. 결과는 JSON 한 벌. * ① 폴더 이름 · 목록 {항목} · 가져오기 POST · 읽기 {종류, 판, 내용} · 고침 PUT {판, 내용}(판 · 409 · 403) * ② 새로 가져오기 · 계산 한 줄 · 입력 목록(한 번만 받음) * ③ 카드 셈: 범위 · 고르기 원래 모양 · 자리 「선택」 만 묶음 * ⚠ 확장자가 `.cjs` 인 까닭은 `helper_b05_curve_radius_floor.cjs` 머리와 같다. */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const ROOT = path.join(__dirname, "..", ".."); const cache = new Map(); const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api" } }; 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 (request.endsWith(".css")) return {}; if (request.startsWith(".")) return loadTs(`${path.resolve(path.dirname(full), request)}.ts`); return require(request); }; new Function("exports", "module", "require", js)(box.exports, box, localRequire); cache.set(full, box.exports); return box.exports; } const reply = (status, body) => ({ status, ok: status >= 200 && status < 300, json: async () => body, }); /** 가짜 서버 — 복사본 폴더마다 부분 파일 · 판(글자) */ const state = { readOnly: false, calls: [], folders: {}, inputsHits: 0 }; const versionOf = (content) => `v${JSON.stringify(content ?? null).length}`; const INPUTS = { 일위대가: "UA000004", 이름: "기슭막이", 로직: [ { 로직: "GA1", 이름: "로직 A", 결과단위: "원/m", 입력: [ { 이름: "지역", 고르기: ["전국평균", "서울"], 자동값: "전국평균" }, { 이름: "소량감률", 단위: "%", 범위: [0, 50], 자동값: 0 }, ], }, { 로직: "GA2", 이름: "로직 B", 입력: [{ 이름: "지역", 고르기: [15, 16] }] }, ], 빠진로직: ["GX"], }; globalThis.fetch = async (url, init = {}) => { const method = init.method ?? "GET"; const body = init.body ? JSON.parse(init.body) : undefined; state.calls.push({ method, url, body }); if (/^\/api\/m02\/layers\/\w+\/items\/[^/]+\/inputs$/.test(url)) { state.inputsHits += 1; return reply(200, INPUTS); } const m = /^\/api\/projects\/([^/]+)\/items(?:\/([^/]+)(?:\/([^/]+))?)?$/.exec(url); if (!m) return reply(404, { detail: "Not Found" }); const [, , folderEnc, partEnc] = m; if (!folderEnc && method === "GET") return reply(200, { 항목: Object.values(state.folders).map((x) => x.info) }); if (folderEnc === "new" && method === "POST") { if (state.noCompany) return reply(409, { detail: "회사부터 등록 — 개인 층은 회사 연결이 필요합니다." }); const item = { layer: "personal", owner: "1/3", key: "SS900001", name: [...body.경로, body.이름].join(" "), }; return reply(200, { key: "SS900001", 이름: item.name, 폴더: body.structure, summary_item: item, }); } if (!folderEnc && method === "POST") { if (!body.structure) return reply(400, { detail: "미리 가져올 구조물(structure)이 필요합니다." }); const folder = body.structure.replace("#", "n"); state.folders[folder] = { info: { 폴더: folder, 원본: body, 마스터바뀜: false, 가져온날: "2026-09-30T01:00:00" }, parts: { 면적: { 시트: [{ id: "s", 칸: { A1: { 값: 1 } } }] }, 입력값: null }, }; return reply(200, { 폴더: folder, 만듦: true, 가져온날: "2026-09-30T01:00:00" }); } const item = state.folders[decodeURIComponent(folderEnc)]; if (!item) return reply(404, { detail: "복사본이 없음" }); const part = partEnc && decodeURIComponent(partEnc); if (part === "reapply") { item.parts.면적 = { 시트: [{ id: "s", 칸: { A1: { 값: 1 } } }] }; return reply(200, { 폴더: item.info.폴더, 가져온날: "2026-09-30T02:00:00", 원본판: "새판", 덮임: ["면적"], }); } if (part === "save-as" && method === "POST") { if (state.noCompany) return reply(409, { detail: "회사부터 등록 — 개인 층은 회사 연결이 필요합니다." }); const name = body.이름 ?? "복사"; return reply(200, { key: "SS900002", 이름: name, summary_item: { layer: "personal", owner: "1/3", key: "SS900002", name }, }); } if (part === "calc") return reply(200, { 줄: [], 계: 12345.6, 빠진입력: [ { 로직: "GA1", 입력: "지역" }, { 로직: "GA2", 입력: "지역" }, ], 멈춤: ["GA1"], }); if (method === "GET") { const content = item.parts[part] ?? null; return reply(200, { 종류: part, 판: versionOf(content), 내용: content }); } if (method === "PUT") { if (state.readOnly) return reply(403, { detail: "프로젝트 편집 권한이 없음" }); const current = versionOf(item.parts[part] ?? null); if (body.판 !== current) return reply(409, { detail: { message: "다른 곳에서 먼저 고쳤습니다", 판: current } }); item.parts[part] = body.내용; return reply(200, { 종류: part, 판: versionOf(body.내용) }); } return reply(404, { detail: "Not Found" }); }; const project = loadTs(path.join(ROOT, "ui_template", "ui_template_item_picker_project.ts")); const model = loadTs(path.join(ROOT, "M02_MasterTemplete", "M02_MasterTemplete_Inputs_Model.ts")); const row = (over = {}) => ({ key: "SS000001", 열: "rv", layer: "system", owner: null, ...over }); async function main() { const out = {}; // 구조물 id → 복사본 폴더(서버 `folder_name`) · 관 지점 id(서버 `pipe@{round(측점, 3)}` 파이썬 float 글) out.folders = ["st-1", "#3", "pipe@85.05"].map(project.structureFolder); out.pipeIds = [85.05, 100, 258.12, 0.1234, 12.0004].map(project.pipeStructureId); const items = project.projectItems("P1"); out.listEmpty = await items.list(); const info = await items.importItem(row({ layer: "personal", owner: "1/3" }), "pipe@85.05"); out.info = info; out.importCall = state.calls.find((c) => c.method === "POST"); out.sameItem = [ project.sameItem(info, row({ layer: "personal", owner: "1/3" })), project.sameItem(info, row({ layer: "personal", owner: "1/3", key: "SS000002" })), project.sameItem(info, row()), ]; await items.importItem(row(), "").then( () => (out.noStructure = "성공"), (error) => (out.noStructure = error.message), ); out.list = (await items.list()).map((x) => x.폴더); out.read = await items.read(info.폴더, "면적"); out.readNullContent = await items.read(info.폴더, "입력값"); // 내용 null = 아직 없음 out.readMissing = await items.read("personal_9-9_SS000009", "면적"); // 없는 폴더 404 = null await items.write(info.폴더, "면적", { 시트: [{ id: "s", 칸: { H10: { 값: 12345 } } }] }); out.afterWrite = state.folders[info.폴더].parts.면적; // 판: 읽은 판으로 씀 · 쓴 뒤 새 판을 들고 있어 이어서 또 쓸 수 있음 await items.write(info.폴더, "면적", { 시트: [{ id: "s", 칸: { H10: { 값: 999 } } }] }); out.secondWrite = state.folders[info.폴더].parts.면적.시트[0].칸.H10; // 읽은 적 없는 부분도 쓰기 전에 판을 먼저 앎 const getsBefore = state.calls.filter((c) => c.method === "GET").length; await items.write(info.폴더, "고른자재", { GA1: { 0: "MT000001" } }); out.writeWithoutReadGets = state.calls.filter((c) => c.method === "GET").length - getsBefore; await items.write(info.폴더, "입력값", { GA1: { 지역: { 값: "서울", 확인: true } } }); out.valuesSaved = state.folders[info.폴더].parts.입력값; // 다른 곳에서 먼저 고침(판이 달라짐) → 409 까닭(`message`) 그대로 state.folders[info.폴더].parts.면적 = { 시트: [], 남이: true }; await items.write(info.폴더, "면적", {}).then( () => (out.stale = "성공"), (error) => (out.stale = error.message), ); state.readOnly = true; await items.write(info.폴더, "입력값", {}).then( () => (out.readOnly = "성공"), (error) => (out.readOnly = error.message), ); state.readOnly = false; const again = await items.reapply(info); out.reapply = again; out.reapplyCall = state.calls .filter((c) => /reapply$/.test(c.url)) .map((c) => `${c.method} ${c.url}`); out.saveAs = await items.saveAs(info.폴더, ["sub5", "경로"], "내 항목"); out.saveAsCall = state.calls.find((c) => /save-as$/.test(c.url)).body; out.newItem = await items.newItem("pipe@85.05", ["개인", "폴더"], "새 항목", "m"); out.newCall = state.calls.find((c) => /items\/new$/.test(c.url)).body; state.noCompany = true; await items.saveAs(info.폴더, [], "x").then( () => (out.noCompanySave = "성공"), (error) => (out.noCompanySave = error.message), ); await items.newItem("pipe@85.05", [], "x", "").then( () => (out.noCompanyNew = "성공"), (error) => (out.noCompanyNew = error.message), ); state.noCompany = false; out.calc = await items.calc(info.폴더); out.calcMissing = await items.calc("없는폴더"); // 입력 목록 — 같은 항목은 한 번만 받음 const first = await project.loadInputs(row()); await project.loadInputs(row()); out.inputsHits = state.inputsHits; out.inputsLogics = first.로직.map((l) => [l.로직, l.입력.map((i) => i.이름)]); // 카드 셈 out.model = { range: [ model.rangeOf({ 이름: "a", 범위: [0, 50] }), model.rangeOf({ 이름: "b", 범위: { 최소: 1, 최대: 2 } }), model.rangeOf({ 이름: "c" }), ], choices: model.choicesOf({ 이름: "d", 고르기: [15, "16", { 값: 18, 이름: "열여덟" }] }), groups: model .pickedGroups(first, { 판: "x", 줄: [ { 로직: "GA1", 입력: "지역", 자리: "선택" }, { 로직: "GA1", 입력: "소량감률", 자리: "면적" }, { 로직: "GA2", 입력: "지역", 자리: "선택" }, ], }) .map((g) => [g.logic.로직, g.inputs.map((i) => i.이름)]), noLink: model.pickedGroups(first, null).length, pills: model.pillsOf( first, { 판: "x", 줄: [{ 로직: "GA1", 입력: "소량감률", 자리: "면적" }] }, "면적", ), }; return out; } main().then( (out) => process.stdout.write(JSON.stringify(out)), (error) => { process.stderr.write(String(error && error.stack ? error.stack : error)); process.exit(1); }, );