- test_item_picker_detail: 복사본 창구(가짜 · 약속 길 · 403 까닭) · 입력 목록 · 카드 순수 함수 · 한 벌 지킴 · 원본 층에 안 씀 - 자동값 · 확인 표시 글자 대비 4.5 이상 · 고르기 값은 원래 모양(숫자)으로 저장 - 새로 가져오기 = 원본에서 온 문서만 덮고 입력값은 남김 - 모달 폭 두 클래스로(공용 모달 폭에 안 밀림) · CAD · 시트 편집 높이 채움 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EauA2ATmqNRbeV48s9CcBr
275 lines
11 KiB
JavaScript
275 lines
11 KiB
JavaScript
/* 항목 고르기 모달 자료 창구 시험 헬퍼(계획서 25-2d) — `ui_template_item_picker_project.ts` 와
|
|
* 선택 입력 카드의 순수 함수를 트랜스파일해 가짜 서버(fetch 대역) 위에서 돌린다. 결과는 JSON 한 벌.
|
|
* ① 가짜 복사본(서버 길 없음): 가져오기 · 고침 · 다시 읽기 · 마스터가 바뀜 · 새로 가져오기 · 원본 그대로
|
|
* ② 실서버 모양(약속 길): 목록 · 읽기 · 고침 PUT · 권한 없음(403) 까닭 그대로 · 가져오기 · 새로 가져오기
|
|
* ③ 입력 목록: 약속 길 그대로 · 없으면 M01 길로 짜 맞춤 · 가짜 연결(모두 선택)
|
|
* ④ 카드 순수 함수: 범위 · 고르기 원래 모양 · 자리 「선택」 만 묶음
|
|
* ⚠ 확장자가 `.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" },
|
|
// 카드 순수 함수만 볼 것 — 화면 부품은 안 씀
|
|
"@ui/ui_template_elements": { el: () => ({}), createButton: () => ({}) },
|
|
"./M02_MasterTemplete_ItemCards": { itemCard: () => ({}), itemNote: () => ({}) },
|
|
};
|
|
|
|
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 store = new Map();
|
|
globalThis.localStorage = {
|
|
getItem: (k) => (store.has(k) ? store.get(k) : null),
|
|
setItem: (k, v) => void store.set(k, String(v)),
|
|
};
|
|
|
|
/** 원본 층(시스템) — 열 rv 의 도면 · 면적 · 재료 · 판은 바뀔 수 있음 */
|
|
const original = {
|
|
version: "v1",
|
|
docs: {
|
|
structure: { 양식: "구조물도면", 도번: "SD01", 도면: { entities: [1, 2] } },
|
|
area: { 면적번: "SA01", 시트: [{ id: "s", 칸: { A1: { 값: 1 } } }] },
|
|
material: { 재료번: "SM01", 시트: [{ id: "s", 칸: {} }] },
|
|
},
|
|
};
|
|
const originalSnapshot = () => JSON.stringify(original);
|
|
|
|
const reply = (status, body) => ({
|
|
status,
|
|
ok: status >= 200 && status < 300,
|
|
json: async () => body,
|
|
});
|
|
|
|
/** 가짜 서버 — mode: "fake"(프로젝트 길 없음) | "real"(약속 길) */
|
|
const state = { mode: "fake", readOnly: false, inputsRoute: false, calls: [], server: {} };
|
|
|
|
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 });
|
|
const m = /^\/api\/m02\/templates\/(structure|area|material)\/(.+)$/.exec(url);
|
|
if (m) {
|
|
return reply(200, {
|
|
종류: m[1],
|
|
이름: decodeURIComponent(m[2]),
|
|
판: original.version,
|
|
문서: JSON.parse(JSON.stringify(original.docs[m[1]])),
|
|
});
|
|
}
|
|
const projects = /^\/api\/projects\/([^/]+)\/items(?:\/([^/]+)(?:\/([^/]+))?)?$/.exec(url);
|
|
if (projects) {
|
|
if (state.mode === "fake") return reply(404, { detail: "Not Found" });
|
|
const [, , folder, part] = projects;
|
|
if (!folder && method === "GET")
|
|
return reply(
|
|
200,
|
|
Object.values(state.server).map((x) => x.info),
|
|
);
|
|
if (!folder && method === "POST") {
|
|
const key = `${body.layer}_${body.owner ?? "none"}_${body.key}`;
|
|
state.server[key] = {
|
|
info: { 폴더: key, 원본: body, 원본판: original.version, 가져온날: "2026-09-29" },
|
|
parts: {
|
|
도면: JSON.parse(JSON.stringify(original.docs.structure)),
|
|
면적: JSON.parse(JSON.stringify(original.docs.area)),
|
|
재료: JSON.parse(JSON.stringify(original.docs.material)),
|
|
},
|
|
};
|
|
return reply(200, state.server[key].info);
|
|
}
|
|
const item = state.server[decodeURIComponent(folder)];
|
|
if (!item) return reply(404, { detail: "없는 폴더" });
|
|
if (part === "refresh" && method === "POST") {
|
|
item.parts.면적 = JSON.parse(JSON.stringify(original.docs.area));
|
|
item.info.원본판 = original.version;
|
|
return reply(200, item.info);
|
|
}
|
|
if (method === "GET") {
|
|
const doc = item.parts[decodeURIComponent(part)];
|
|
return doc === undefined ? reply(404, { detail: "부분 없음" }) : reply(200, doc);
|
|
}
|
|
if (method === "PUT") {
|
|
if (state.readOnly) return reply(403, { detail: "프로젝트 편집 권한이 없음" });
|
|
item.parts[decodeURIComponent(part)] = body;
|
|
return reply(200, { ok: true });
|
|
}
|
|
}
|
|
const inputs = /^\/api\/m02\/layers\/(\w+)\/items\/([^/]+)\/inputs/.exec(url);
|
|
if (inputs) {
|
|
return state.inputsRoute
|
|
? reply(200, { 일위대가: "UAREAL", 이름: "실서버", 로직: [], 빠진로직: [] })
|
|
: reply(404, { detail: "Not Found" });
|
|
}
|
|
if (url.startsWith("/api/m01/combo?key=")) {
|
|
return reply(200, {
|
|
combo: { 이름: "기슭막이", 담은로직: [{ 로직: "GA1" }, { 로직: "GA2" }, { 로직: "GX" }] },
|
|
});
|
|
}
|
|
if (url.startsWith("/api/m01/logic/auto?key=")) {
|
|
return reply(200, { 값: { 지역: "전국평균", 소량감률: 0 } });
|
|
}
|
|
const logic = /^\/api\/m01\/logic\?key=(\w+)/.exec(url);
|
|
if (logic) {
|
|
if (logic[1] === "GX") return reply(404, { detail: "없는 로직" });
|
|
return reply(200, {
|
|
logic: {
|
|
이름: `로직 ${logic[1]}`,
|
|
결과단위: "원/m",
|
|
입력: [
|
|
{ 이름: "지역", 고르기: ["전국평균", "서울"] },
|
|
{ 이름: "소량감률", 단위: "%", 범위: [0, 50] },
|
|
],
|
|
},
|
|
});
|
|
}
|
|
return reply(404, { detail: "Not Found" });
|
|
};
|
|
|
|
const project = loadTs(path.join(ROOT, "ui_template", "ui_template_item_picker_project.ts"));
|
|
const card = loadTs(path.join(ROOT, "M02_MasterTemplete", "M02_MasterTemplete_InputsCard.ts"));
|
|
|
|
const row = (over = {}) => ({
|
|
key: "SS000001",
|
|
열: "rv",
|
|
layer: "system",
|
|
owner: null,
|
|
일위대가: "UA1",
|
|
...over,
|
|
});
|
|
const clone = (x) => JSON.parse(JSON.stringify(x));
|
|
|
|
async function main() {
|
|
const out = {};
|
|
|
|
// ① 폴더 이름 — 서버 `snapshot_folder_name` 과 같은 글
|
|
out.folders = [
|
|
project.folderOf(row()),
|
|
project.folderOf(row({ layer: "company", owner: "3", key: "SS000001" })),
|
|
project.folderOf(row({ layer: "personal", owner: "3/12", key: "SS000002" })),
|
|
];
|
|
|
|
// ② 가짜 복사본 — 서버 길이 없을 때
|
|
state.mode = "fake";
|
|
const fake = project.projectItems("P-FAKE");
|
|
const before = originalSnapshot();
|
|
out.fake = { list: await fake.list() };
|
|
out.fake.isFake = fake.isFake();
|
|
const info = await fake.importItem(row());
|
|
out.fake.info = info;
|
|
out.fake.readBack = await fake.read(info.폴더, "면적");
|
|
await fake.write(info.폴더, "면적", {
|
|
...out.fake.readBack,
|
|
시트: [{ id: "s", 칸: { H10: { 값: 12345 } } }],
|
|
});
|
|
await fake.write(info.폴더, "입력값", { GA1: { 지역: { 값: "서울", 확인: true } } });
|
|
out.fake.afterWrite = await fake.read(info.폴더, "면적");
|
|
out.fake.listAfter = (await fake.list()).map((one) => one.폴더);
|
|
out.fake.changedNow = await fake.masterChanged(row(), info);
|
|
original.version = "v2";
|
|
out.fake.changedAfterMaster = await fake.masterChanged(row(), info);
|
|
await fake.refresh(row(), info);
|
|
out.fake.afterRefresh = await fake.read(info.폴더, "면적");
|
|
out.fake.valuesKept = await fake.read(info.폴더, "입력값");
|
|
out.fake.changedAfterRefresh = await fake.masterChanged(row(), (await fake.list())[0]);
|
|
out.fake.originalUntouched = before.replace('"v1"', '"v2"') === originalSnapshot();
|
|
out.fake.storageKeys = [...store.keys()];
|
|
await fake.write("없는폴더", "면적", {}).then(
|
|
() => (out.fake.writeMissing = "성공"),
|
|
(error) => (out.fake.writeMissing = error.message),
|
|
);
|
|
original.version = "v1";
|
|
|
|
// ③ 실서버 모양 — 약속 길
|
|
state.mode = "real";
|
|
state.calls = [];
|
|
const real = project.projectItems("P-REAL");
|
|
out.real = { list: await real.list(), isFake: real.isFake() };
|
|
const rinfo = await real.importItem(row({ layer: "personal", owner: "1/3" }));
|
|
out.real.info = rinfo;
|
|
out.real.importCall = state.calls.find((c) => c.method === "POST");
|
|
out.real.read = await real.read(rinfo.폴더, "면적");
|
|
out.real.readMissing = await real.read(rinfo.폴더, "고른자재");
|
|
await real.write(rinfo.폴더, "입력값", { L: { 이름: { 값: 1, 확인: true } } });
|
|
out.real.putCall = state.calls.filter((c) => c.method === "PUT").map((c) => c.url);
|
|
state.readOnly = true;
|
|
await real.write(rinfo.폴더, "면적", {}).then(
|
|
() => (out.real.readOnlyWrite = "성공"),
|
|
(error) => (out.real.readOnlyWrite = error.message),
|
|
);
|
|
state.readOnly = false;
|
|
await real.refresh(row({ layer: "personal", owner: "1/3" }), rinfo);
|
|
out.real.refreshCall = state.calls
|
|
.filter((c) => /refresh$/.test(c.url))
|
|
.map((c) => `${c.method} ${c.url}`);
|
|
out.real.changedFlag = await real.masterChanged(row(), { ...rinfo, 마스터바뀜: true });
|
|
|
|
// ④ 입력 목록
|
|
state.inputsRoute = false;
|
|
const madeUp = await project.loadInputs(row({ key: "SS000009" }));
|
|
out.inputs = {
|
|
fake: madeUp.fake,
|
|
logics: madeUp.list.로직.map((l) => [l.로직, l.입력.map((i) => i.이름)]),
|
|
missing: madeUp.list.빠진로직,
|
|
auto: madeUp.list.로직[0].입력.map((i) => i.자동값),
|
|
link: project.fakeLink(madeUp.list).줄.length,
|
|
linkPlaces: [...new Set(project.fakeLink(madeUp.list).줄.map((r) => r.자리))],
|
|
};
|
|
state.inputsRoute = true;
|
|
const realInputs = await project.loadInputs(row({ key: "SS000010" }));
|
|
out.inputs.real = { fake: realInputs.fake, name: realInputs.list.이름 };
|
|
|
|
// ⑤ 카드 순수 함수
|
|
out.card = {
|
|
range: [
|
|
card.rangeOf({ 이름: "a", 범위: [0, 50] }),
|
|
card.rangeOf({ 이름: "b", 범위: { 최소: 1, 최대: 2 } }),
|
|
card.rangeOf({ 이름: "c" }),
|
|
],
|
|
choices: card.choicesOf({ 이름: "d", 고르기: [15, "16", { 값: 18, 이름: "열여덟" }] }),
|
|
keys: [card.inputKey("GA1", "지역"), card.pillKey("GA1", "지역")],
|
|
groups: card
|
|
.pickedGroups(madeUp.list, {
|
|
판: "x",
|
|
줄: [
|
|
{ 로직: "GA1", 입력: "지역", 자리: "선택" },
|
|
{ 로직: "GA1", 입력: "소량감률", 자리: "면적" },
|
|
{ 로직: "GA2", 입력: "소량감률", 자리: "선택" },
|
|
],
|
|
})
|
|
.map((g) => [g.logic.로직, g.inputs.map((i) => i.이름)]),
|
|
noLink: card.pickedGroups(madeUp.list, null).length,
|
|
};
|
|
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);
|
|
},
|
|
);
|