- 한눈 표(M02_SO_Def_Table): 군 · 종류 ▲▼ · 이름 · 목록 · 자리꼴 · 정본(고정) · 횡단도 · 카드 이름 · 형상(+ 벽 기본 형태 · 물빼기) · 알약 색 · 3D · 검사 · 처음 값 = 흐림 「기본」 · 바꿈 = 진함 · [처음 값으로] - 종류별: 반영 칸 + 명세 이름 나무 줄마다 보임(컨테이너 = 그 안 칸) · 못 숨김 · 미리보기 숨김 따름 · 「안 씀」 = 목록 B06 만 - 부품 탭(M02_SO_Def_Parts): 부품 · 읽는 칸 · 자리꼴 · 정본 · 쓰는 종류 · 켬/끔(정의 부품 = 끈 것만) - 편집 손질(M02_SO_Def_Reflect): 처음 값이면 반영에서 지움 · [저장] 때 enabled = 목록 both 일 때만 참 - 검사 짝 M02_SO_Def_Check.ts ↔ M02_SO_Check.py(글까지 같음): 반영 · 부품 꼴 · 형상 none → 횡단도 none · 부품 자리꼴 · 정본 · 기본값 없는 읽는 칸 새로 숨김 금지(앞 판 숨김 그대로) · 종류 지우기 · 자리꼴 · 정본 · 군 글자 · 선택지 값 고정 · 서버 check(doc, 지금 판) · 앞 판 없으면 꼴만 - 명세 폼: 쓰는 옵션 키가 모두 숨긴 칸 · 상자는 안 그림(값 그대로) · B05 · B06 시설 · 구조물 명세 폼이 정의 「표시」를 따름 - 판 알림: GET 구조물 목록에 definition_version · structures.json 판 ≠ 지금 판이면 「구조물 정의가 판 N 으로 바뀜 — 저장하면 반영」 한 번 - 시험: test_322_2_check_mirror(거울 · 규칙 · 반영 손질 · 서버 저장 · 422) · 판 알림 · 옛 정의 화면 시험 = 선택지 값 대신 이름 표 · 못 숨김 아닌 칸 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
348 lines
13 KiB
JavaScript
348 lines
13 KiB
JavaScript
/* PLAN 322-0 전수 표 시험 헬퍼 — 42종 × 견본(구조물 · 관 지점 · 빈칸)마다 지금 화면 함수 결과를 표준출력 JSON 한 벌로.
|
|
* 측점(`structureStationMarks` · `pipeStationMarks`) · 카드 이름 · 형상 열쇠(`Section_Store` 초안 얹기 → 미리 뜬 횡단 ·
|
|
* 벽 · 노면 · 맹암거) · 3D 몸체(`buildCorridorStructures`) · 알약 띠(`buildStructureLane`) · 고르기 목록(B05 · B06 `pickGroups`).
|
|
* 입력 = argv[2] JSON(서버 꼴 types · 정의 · 견본 · 격자 · 맨 횡단) — 파이썬 짝이 만듦. 짝 = `test_322_0_golden.py`.
|
|
* 진짜 TS 를 그 자리에서 트랜스파일해 Node 로 돌림(서버 부르는 곳 · 무거운 화면 모듈만 흉내).
|
|
*/
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const ROOT = path.join(__dirname, "..", "..");
|
|
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
|
|
const input = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
const clone = (v) => JSON.parse(JSON.stringify(v));
|
|
|
|
/* ── 브라우저 흉내 ─────────────────────────────────────────────────────── */
|
|
class FakeStorage {
|
|
constructor() {
|
|
this.map = new Map();
|
|
}
|
|
get length() {
|
|
return this.map.size;
|
|
}
|
|
key(i) {
|
|
return [...this.map.keys()][i] ?? null;
|
|
}
|
|
getItem(k) {
|
|
return this.map.has(k) ? this.map.get(k) : null;
|
|
}
|
|
setItem(k, v) {
|
|
this.map.set(k, String(v));
|
|
}
|
|
removeItem(k) {
|
|
this.map.delete(k);
|
|
}
|
|
clear() {
|
|
this.map.clear();
|
|
}
|
|
}
|
|
class FakeElement {
|
|
constructor(tag) {
|
|
this.tagName = tag;
|
|
this.className = "";
|
|
this.style = {};
|
|
this.dataset = {};
|
|
this.children = [];
|
|
this.textContent = "";
|
|
this.classList = { toggle: () => {}, add: () => {}, remove: () => {}, contains: () => false };
|
|
}
|
|
append(...nodes) {
|
|
this.children.push(...nodes);
|
|
}
|
|
prepend(...nodes) {
|
|
this.children.unshift(...nodes);
|
|
}
|
|
appendChild(node) {
|
|
this.children.push(node);
|
|
return node;
|
|
}
|
|
addEventListener() {}
|
|
setAttribute() {}
|
|
}
|
|
globalThis.window = {
|
|
sessionStorage: new FakeStorage(),
|
|
localStorage: new FakeStorage(),
|
|
addEventListener: () => {},
|
|
removeEventListener: () => {},
|
|
dispatchEvent: () => true,
|
|
requestAnimationFrame: (fn) => fn(),
|
|
setTimeout,
|
|
clearTimeout,
|
|
location: { origin: "http://localhost", pathname: "/" },
|
|
};
|
|
globalThis.sessionStorage = window.sessionStorage;
|
|
globalThis.localStorage = window.localStorage;
|
|
globalThis.requestAnimationFrame = (fn) => fn();
|
|
const docListeners = new Map();
|
|
globalThis.document = {
|
|
activeElement: null,
|
|
hidden: false,
|
|
visibilityState: "visible",
|
|
addEventListener: (type, fn) => docListeners.set(type, fn),
|
|
removeEventListener: (type, fn) => docListeners.get(type) === fn && docListeners.delete(type),
|
|
createElement: (tag) => new FakeElement(tag),
|
|
};
|
|
/** 서버 흉내 — 정의 판(`server.version`) · 종류 받은 횟수(`server.typeCalls`). */
|
|
const server = { version: input.definition.판, typeCalls: 0 };
|
|
globalThis.fetch = async (url) => {
|
|
const at = String(url);
|
|
if (at.includes("/structure-types")) server.typeCalls += 1;
|
|
const body = at.includes("structure-definition/version")
|
|
? { 판: server.version }
|
|
: at.includes("structure-definition")
|
|
? { ...clone(input.definition), 판: server.version }
|
|
: at.includes("/route/structures")
|
|
? { status: "success", project_id: PROJECT, revision: 0, structures: [] }
|
|
: { status: "success", schema_version: 1, types: clone(input.types) };
|
|
return { ok: true, status: 200, json: async () => body };
|
|
};
|
|
|
|
/* ── TS 싣기 — 흉내 파일만 갈음 ──────────────────────────────────────────── */
|
|
const file = (...parts) => path.resolve(ROOT, ...parts);
|
|
const B05 = (name) => file("B05_Profile", `${name}.ts`);
|
|
const B06 = (name) => file("B06_Section", `${name}.ts`);
|
|
const PROJECT = "P322";
|
|
let pendingPipes = null;
|
|
const STUBS = new Map([
|
|
[
|
|
B06("B06_Section_Api_Fetch"),
|
|
{
|
|
fetchSectionDetail: async () => currentDetail(),
|
|
saveSections: async () => ({}),
|
|
previewStructureSections: async (_p, chainages) =>
|
|
chainages.map((at) => ({ ...plainAt(at), kind: "irregular" })),
|
|
},
|
|
],
|
|
[B06("B06_Section_Cross_Refresh"), { USER_TOUCHED_KEYS: [] }],
|
|
[
|
|
B06("B06_Section_Api_Culvert_Options"),
|
|
{ kindChangedChainages: () => [], STRUCTURE_ADJUST_KEYS: [] },
|
|
],
|
|
[
|
|
B06("B06_Section_Structure_Layouts"),
|
|
{ applyStructureAreaRows: () => {}, structureAreaRows: () => [] },
|
|
],
|
|
[
|
|
B05("B05_Profile_Api_Pipes_Draft"),
|
|
{ readPendingPipes: () => pendingPipes, writePendingPipes: () => {} },
|
|
],
|
|
[
|
|
file("B04_PreProcess", "B04_PreProcess_Api_Fetch.ts"),
|
|
{ fetchDetailPipePoints: async () => ({ pipe_points: [] }), noteSnapshotFailed: () => {} },
|
|
],
|
|
[file("A00_Common", "touch_bridge.ts"), { pickPx: () => 6 }],
|
|
[file("ui_template", "ui_template_toast.ts"), { showToast: () => {} }],
|
|
[file("ui_template", "ui_template_elements.ts"), { showToast: () => {} }],
|
|
]);
|
|
const resolveFile = (base) =>
|
|
[base, `${base}.ts`, base.replace(/\.js$/, ".ts"), path.join(base, "index.ts")].find(
|
|
(p) => fs.existsSync(p) && fs.statSync(p).isFile(),
|
|
) ?? `${base}.ts`;
|
|
const cache = new Map();
|
|
function loadTs(target) {
|
|
const full = path.resolve(target);
|
|
if (STUBS.has(full)) return STUBS.get(full);
|
|
if (cache.has(full)) return cache.get(full);
|
|
if (full.endsWith(".json")) {
|
|
const data = JSON.parse(fs.readFileSync(full, "utf8"));
|
|
cache.set(full, data);
|
|
return data;
|
|
}
|
|
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
|
compilerOptions: {
|
|
module: ts.ModuleKind.CommonJS,
|
|
target: ts.ScriptTarget.ES2020,
|
|
esModuleInterop: true,
|
|
},
|
|
}).outputText;
|
|
const box = { exports: {} };
|
|
cache.set(full, box.exports);
|
|
const need = (spec) => {
|
|
if (spec === "three") return require(path.join(ROOT, "config", "node_modules", "three"));
|
|
const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" };
|
|
for (const [prefix, dir] of Object.entries(alias))
|
|
if (spec.startsWith(prefix))
|
|
return loadTs(resolveFile(path.join(ROOT, dir, spec.slice(prefix.length))));
|
|
if (spec.startsWith(".")) return loadTs(resolveFile(path.join(path.dirname(full), spec)));
|
|
return require(spec);
|
|
};
|
|
new Function("exports", "module", "require", js)(box.exports, box, need);
|
|
return box.exports;
|
|
}
|
|
|
|
/* ── 견본 횡단 ───────────────────────────────────────────────────────────── */
|
|
const plainAt = (at) => ({ ...clone(input.plain_section), chainage_m: at, kind: "regular" });
|
|
const currentDetail = () => ({
|
|
longitudinal: { stations: input.grid.map((at) => ({ chainage_m: at, kind: "regular" })) },
|
|
cross_sections: input.grid.map(plainAt),
|
|
});
|
|
const frameAtOf = (sections) => (atM) => {
|
|
const s = sections.reduce((b, x) =>
|
|
Math.abs(x.chainage_m - atM) < Math.abs(b.chainage_m - atM) ? x : b,
|
|
);
|
|
const [lx, ly] = s.frame.left_xy;
|
|
const along = atM - s.chainage_m;
|
|
return {
|
|
cx: s.center_x - ly * along,
|
|
cy: s.center_y + lx * along,
|
|
leftX: lx,
|
|
leftY: ly,
|
|
designZ: s.design?.design_elevation_m ?? null,
|
|
};
|
|
};
|
|
|
|
/** 횡단 하나에 얹힌 구조물 그림 자료 — 열쇠 + 고르는 데 쓰는 값만(파이썬 짝 `_shape_of` 와 같은 꼴). */
|
|
const SHAPE_KEYS = [
|
|
"culvert",
|
|
"ford",
|
|
"box",
|
|
"ford_pavement",
|
|
"revetment",
|
|
"pavement",
|
|
"underdrain",
|
|
];
|
|
function shapeOf(section) {
|
|
const out = {};
|
|
for (const key of SHAPE_KEYS) {
|
|
const spec = section[key];
|
|
if (!spec) continue;
|
|
if (key === "culvert") out[key] = { hidden_pipe: !!spec.hidden_pipe, body: !!spec.body };
|
|
else if (key === "revetment")
|
|
out[key] = { type_id: spec.type_id ?? null, form: spec.form ?? null };
|
|
else if (key === "pavement") out[key] = spec.map((p) => p.type_id).sort();
|
|
else out[key] = true;
|
|
}
|
|
return out;
|
|
}
|
|
const at2 = (n) => Number(n).toFixed(2);
|
|
|
|
const { structureStationMarks, pipeStationMarks } = loadTs(
|
|
file("common_util", "common_util_structure_station.ts"),
|
|
);
|
|
const {
|
|
dropStaleStructureTypes,
|
|
fetchStructureTypes,
|
|
onStaleStructureTypes,
|
|
pipesToStructureMarks,
|
|
staleDefinitionNotice,
|
|
writePendingStructures,
|
|
} = loadTs(B05("B05_Profile_Api_Structures"));
|
|
const { loadSectionDetail } = loadTs(B06("B06_Section_Section_Store"));
|
|
const { forgetDraftStations } = loadTs(B06("B06_Section_Section_Drafts"));
|
|
const { buildCorridorStructures } = loadTs(B05("B05_Profile_UI_Corridor_Structures"));
|
|
const { computeRevetmentLayout } = loadTs(B06("B06_Section_UI_Cross_Revetment"));
|
|
const { buildStructureLane } = loadTs(B05("B05_Profile_UI_Structures_Marks"));
|
|
const { pickGroups } = loadTs(B05("B05_Profile_UI_Structures_Panel_Types"));
|
|
const { structureDisplayName } = loadTs(B06("B06_Section_UI_Section_Common"));
|
|
const { listPick } = loadTs(file("common_util", "common_util_structure_catalog.ts"));
|
|
|
|
async function main() {
|
|
const types = await fetchStructureTypes(true);
|
|
const byId = new Map(types.map((type) => [type.type_id, type]));
|
|
const out = { samples: {}, pick: {} };
|
|
|
|
// 고르기 목록 — 페이지가 넘기는 거르기 그대로(B05 = 켜진 것 · B06 = 전부)
|
|
const pagePicks = { b05: listPick("b05"), b06: listPick("b06") }; // 페이지 거르기 = 표 목록(322-1)
|
|
for (const [page, pick] of Object.entries(pagePicks)) {
|
|
const seen = {};
|
|
for (const group of pickGroups(types, pick))
|
|
for (const member of group.types)
|
|
seen[member.type.type_id] = group.hidden || member.hidden ? "hidden" : "shown";
|
|
for (const type of types)
|
|
(out.pick[type.type_id] ??= {})[page] = seen[type.type_id] ?? "absent";
|
|
}
|
|
|
|
let route = 0;
|
|
for (const sample of input.samples) {
|
|
route += 1;
|
|
const type = byId.get(sample.type_id);
|
|
const row = {};
|
|
// 측점
|
|
const marks = sample.pipe
|
|
? pipeStationMarks(sample.pipe)
|
|
: structureStationMarks(sample.structure, type);
|
|
row.station = marks.map((m) => `${m.role}@${at2(m.chainage_m)}`);
|
|
|
|
// 초안 얹기(Section_Store) → 카드 이름 · 형상 열쇠
|
|
window.sessionStorage.clear();
|
|
forgetDraftStations(PROJECT);
|
|
pendingPipes = sample.pipe ? [clone(sample.pipe)] : null;
|
|
writePendingStructures(PROJECT, sample.structure ? [clone(sample.structure)] : null);
|
|
const detail = await loadSectionDetail(PROJECT, route, { force: true });
|
|
const touched = detail.cross_sections.filter(
|
|
(section) => section.structure || Object.keys(shapeOf(section)).length,
|
|
);
|
|
row.cards = Object.fromEntries(
|
|
touched.map((section) => [
|
|
at2(section.chainage_m),
|
|
{
|
|
title: section.structure ?? null,
|
|
display: section.structure ? structureDisplayName(section.structure) : null,
|
|
shape: shapeOf(section),
|
|
},
|
|
]),
|
|
);
|
|
// 벽 물빼기(옹벽만 · 형상칸.weep)
|
|
row.weep = touched
|
|
.filter((section) => section.revetment)
|
|
.some((section) =>
|
|
(computeRevetmentLayout(section, undefined)?.walls ?? []).some((wall) => wall.retaining),
|
|
);
|
|
// 3D 몸체
|
|
const solids = touched.length ? buildCorridorStructures(touched, frameAtOf(touched)) : [];
|
|
row.model3d = [...new Set(solids.map((s) => `${s.kind}:${s.key ?? ""}`))].sort();
|
|
|
|
// 알약 띠
|
|
const lane = buildStructureLane({
|
|
structures: sample.pipe
|
|
? pipesToStructureMarks([sample.pipe])
|
|
: [{ ...sample.structure, structure_id: "s1" }],
|
|
types,
|
|
x: (m) => m,
|
|
chainageAt: (px) => px,
|
|
maxChainageM: 400,
|
|
widthPx: 400,
|
|
axisWidthPx: 40,
|
|
stationIntervalM: 20,
|
|
selectedId: null,
|
|
onSelect: () => {},
|
|
});
|
|
row.band = lane.children.filter((el) => el.className.startsWith("b05-structure__band")).length;
|
|
out.samples[`${sample.type_id}/${sample.case}`] = row;
|
|
}
|
|
// 판 확인(322-0 ③) — 같은 판 = 캐시 그대로 · 바뀐 판 = 버리고 다시 받음 · 탭이 다시 보이면 알림
|
|
await fetchStructureTypes(true);
|
|
server.typeCalls = 0;
|
|
const same = await dropStaleStructureTypes();
|
|
await fetchStructureTypes(true);
|
|
const sameCalls = server.typeCalls;
|
|
let reloads = 0;
|
|
const off = onStaleStructureTypes(() => (reloads += 1));
|
|
server.version += 1;
|
|
docListeners.get("visibilitychange")?.();
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
await fetchStructureTypes(true);
|
|
const staleCalls = server.typeCalls - sameCalls;
|
|
off();
|
|
// 판 알림(322-2) — 프로젝트 정본이 옛 판으로 적혔으면 한 번만 · 같은 판 · 판 없음은 안 알림
|
|
const old = server.version - 1;
|
|
out.notice = [
|
|
staleDefinitionNotice(PROJECT, old),
|
|
staleDefinitionNotice(PROJECT, old),
|
|
staleDefinitionNotice(PROJECT, server.version),
|
|
staleDefinitionNotice(PROJECT, null),
|
|
];
|
|
out.version = {
|
|
same,
|
|
sameCalls,
|
|
reloads,
|
|
staleCalls,
|
|
removed: !docListeners.has("visibilitychange"),
|
|
};
|
|
process.stdout.write(JSON.stringify(out));
|
|
}
|
|
|
|
main().catch((error) => {
|
|
process.stderr.write(String(error?.stack ?? error));
|
|
process.exit(1);
|
|
});
|