Files
Aislo/resources/tester/helper_item_picker_project.cjs
T
eomsangdonandClaude Sonnet 5.5 3a59f715f6 feat(ui_template): 항목 고르기 모달을 실서버에 이음 · 선택 입력 카드는 sub2 파일 한 벌 (PLAN 25-2d)
- 복사본 창구를 sub6 길 모양({항목} · {종류, 판, 내용} · PUT {판, 내용} · 409 message · refresh · calc)으로 — 가짜 응답 지움
- 입력 목록 · 항목 연결은 sub4 길(fetchItemInputs · readInputLinks) · 알약은 pillsOf
- 값 받기 카드를 M02_MasterTemplete_Inputs.ts 에 모드 view · value 로 더함(InputsCard 지움 · 두 벌 금지) · 셈은 _Inputs_Model
- 상태 줄에 복사본 계산 한 줄(계 · 빠진 입력) · 목록은 모달에서 한 번만 받아 들고 있음
- Structure.ts: sub2 의 선택 입력 카드를 ItemCards 의 extraCards 로 다시 얹음

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EauA2ATmqNRbeV48s9CcBr
2026-09-30 00:06:37 +09:00

216 lines
9.0 KiB
JavaScript

/* 항목 고르기 모달 자료 창구 시험 헬퍼(계획서 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 && method === "POST") {
const folder = `${body.layer}_${(body.owner ?? "none").replace(/\//g, "-")}_${body.key}`;
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 === "refresh") {
item.parts.면적 = { 시트: [{ id: "s", 칸: { A1: { 값: 1 } } }] };
return reply(200, { 폴더: item.info.폴더, 가져온날: "2026-09-30T02:00:00", 원본판: "새판" });
}
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 = {};
out.folders = [
project.folderOf(row()),
project.folderOf(row({ layer: "company", owner: "3" })),
project.folderOf(row({ layer: "personal", owner: "3/12", key: "SS000002" })),
];
const items = project.projectItems("P1");
out.listEmpty = await items.list();
const info = await items.importItem(row({ layer: "personal", owner: "1/3" }));
out.info = info;
out.importCall = state.calls.find((c) => c.method === "POST");
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 fresh = await items.refresh(info);
out.refresh = fresh;
out.refreshCall = state.calls
.filter((c) => /refresh$/.test(c.url))
.map((c) => `${c.method} ${c.url}`);
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);
},
);