- 넣을 값 카드(M02_Labor_Values)는 담은 로직 입력 전부를 보임(자리 「선택」 감싸기 없앰) · 이름 「넣을 값」 - 고르기 모달 면적산출 알약 = 복사본 일위대가 알약(가져온 때 그대로) - 입력값.json · 고른자재.json · /calc 는 그대로 · storage 층 inputs/ 파일은 안 건드림(읽기만 끊음) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
281 lines
12 KiB
JavaScript
281 lines
12 KiB
JavaScript
/* 항목 고르기 모달 자료 창구 시험 헬퍼(계획서 25-2d) — `ui_template_item_picker_project.ts` 와
|
|
* 넣을 값 셈(`M02_Labor_Values_Model.ts`)을 트랜스파일해 가짜 서버(fetch 대역) 위에서 돌린다.
|
|
* 서버 모양은 sub6 복사본 길(`M02_Project_Items_Router.py`) · sub4 입력 길 그대로. 결과는 JSON 한 벌.
|
|
* ① 폴더 이름 · 목록 {항목} · 가져오기 POST · 읽기 {종류, 판, 내용} · 고침 PUT {판, 내용}(판 · 409 · 403)
|
|
* ② 새로 가져오기 · 계산 한 줄
|
|
* ③ 값 셈: 범위 · 고르기 원래 모양
|
|
* ④ 일위대가(39-1c): 복사본 요약 · 원본 층 계산 요약 · 넣을 값 = 담은 로직 입력 전부
|
|
* ⚠ 확장자가 `.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: {} };
|
|
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 (url.startsWith("/api/m02/logic-lib/inputs?keys=")) {
|
|
state.logicKeys = decodeURIComponent(url.split("=")[1]).split(",");
|
|
return reply(200, { 로직: INPUTS.로직, 빠진로직: [] });
|
|
}
|
|
if (/^\/api\/m02\/layers\/system\/labor\/rv\/calc$/.test(url)) {
|
|
return reply(200, {
|
|
줄: [
|
|
{ 로직: "GA1", 까닭: null },
|
|
{ 로직: "GA2", 까닭: "입력 없음" },
|
|
],
|
|
자재: [{ 코드: "MT000008" }],
|
|
기준합: { 계: 500 },
|
|
반영합: { 계: 1500 },
|
|
});
|
|
}
|
|
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,
|
|
일위대가: {
|
|
줄: { 일위대가번: "SU-000007", 담은로직: [{ 로직: "GA1" }, { 로직: "GA2" }], 자재: [{}] },
|
|
},
|
|
},
|
|
};
|
|
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"],
|
|
기준합: { 계: 100 },
|
|
반영합: { 계: 300 },
|
|
});
|
|
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_Labor_Values_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("없는폴더");
|
|
|
|
// 일위대가 — 복사본 요약(복사본 계산) · 원본 층 요약(층 calc) · 넣을 값 = 담은 로직 입력 전부
|
|
out.laborCopy = await project.laborOf(row(), items, info.폴더);
|
|
out.laborOrigin = await project.laborOf(row(), null, null);
|
|
out.laborNone = await project.laborOf(row({ 열: "없는열" }), null, null);
|
|
const valueData = await project.laborValueData(["GA1", "GA2"], { GA1: {} });
|
|
out.laborValues = {
|
|
keys: state.logicKeys,
|
|
logics: valueData.list.로직.map((l) => [l.로직, l.입력.map((i) => i.이름)]),
|
|
hasLink: "link" in valueData,
|
|
values: valueData.values,
|
|
};
|
|
|
|
// 값 셈
|
|
out.model = {
|
|
range: [
|
|
model.rangeOf({ 이름: "a", 범위: [0, 50] }),
|
|
model.rangeOf({ 이름: "b", 범위: { 최소: 1, 최대: 2 } }),
|
|
model.rangeOf({ 이름: "c" }),
|
|
],
|
|
choices: model.choicesOf({ 이름: "d", 고르기: [15, "16", { 값: 18, 이름: "열여덟" }] }),
|
|
key: model.inputKey("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);
|
|
},
|
|
);
|