/* 462 B05 · B06 한 페이지 시설 줄 한 벌(요구 271-5) — 진짜 `design_facility_lane` · 관 id 장부 · 측점 계획 · * 관 따라감 · 가게를 Node 로 돌려 JSON 한 줄(`test_271_facility_lane.py` 가 판정). 종단도는 가짜(받은 것 기록). * node helper_271_facility_lane.cjs */ const fs = require("fs"); const path = require("path"); const ROOT = path.join(__dirname, "..", ".."); const { makeLoader } = require("./helper_ts_stub_loader.cjs"); const CATALOG = require(path.join(__dirname, "helper_322_catalog_module.cjs")); function memoryStorage() { const map = new Map(); return { getItem: (k) => (map.has(k) ? map.get(k) : null), setItem: (k, v) => map.set(k, String(v)), removeItem: (k) => map.delete(k), key: (i) => [...map.keys()][i] ?? null, get length() { return map.size; }, }; } globalThis.window = { sessionStorage: memoryStorage(), localStorage: memoryStorage() }; globalThis.sessionStorage = globalThis.window.sessionStorage; globalThis.localStorage = globalThis.window.localStorage; const anyModule = (extra = {}) => new Proxy(extra, { get: (t, key) => (key in t ? t[key] : () => undefined) }); const STUBS = { "config_frontend.ts": anyModule({ CURRENT_PROJECT_ID_KEY: "frd_current_project_id", API_BASE_URL: "", API_TIMEOUT_MS: 1000, PIPE_TYPES: [], }), "ui_template_elements.ts": anyModule(), "B04_PreProcess_Api_Fetch.ts": anyModule(), // 소단 사본(467) — 끌어오는 줄이 import.meta 를 써 이 로더로 못 읽음 · 소단 몫은 test_272_berm_source(vite 묶음) "B06_Section_UI_Page_Persist.ts": anyModule({ bermSpansFromStructures: () => [] }), }; const resolve = (base) => [base, `${base}.ts`, path.join(base, "index.ts")].find( (p) => fs.existsSync(p) && fs.statSync(p).isFile(), ) ?? `${base}.ts`; const { loadTs } = makeLoader({ root: ROOT, interop: true, fileStub: (full) => STUBS[path.basename(full)], requireFor: (full) => (request) => { if (CATALOG.isCatalogRequest(request)) return CATALOG; if (request.endsWith(".json")) return JSON.parse(fs.readFileSync(path.join(path.dirname(full), request), "utf8")); const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" }; for (const [prefix, dir] of Object.entries(alias)) if (request.startsWith(prefix)) return loadTs(resolve(path.join(ROOT, dir, request.slice(prefix.length)))); return loadTs(resolve(path.join(path.dirname(full), request))); }, }); const design = path.join(ROOT, "A00_Common", "design"); const { createFacilityLane } = loadTs(path.join(design, "design_facility_lane.ts")); const ids = loadTs(path.join(design, "design_pipe_ids.ts")); const { useDesignStore } = loadTs(path.join(design, "design_store.ts")); const { readState } = loadTs(path.join(ROOT, "A00_Common", "b_page_state.ts")); const TYPES = JSON.parse( fs.readFileSync(path.join(ROOT, "resources", "master_template", "구조물정의.json"), "utf8"), ).types; /** 가짜 종단도 — 받은 것 · 편집 옮김 기록. */ function fakeProfile() { const got = { structures: null, stations: null, minCover: null, types: 0, resets: [], stationWrites: 0, }; return { got, setStructureTypes: (types) => (got.types = types.length), setStructures: (next) => (got.structures = next), setIrregularStations: (stations) => ((got.stations = stations), (got.stationWrites += 1)), setMinCoverPipes: (pipes) => (got.minCover = pipes.map((p) => p.chainage_m)), resetStationEdit: (from, to) => got.resets.push([from, to ?? null]), }; } const book = (projectId) => readState("pipe-ids", projectId) ?? { next: 1, ids: [] }; const wall = (id, at) => ({ structure_id: id, type_id: "retaining_wall", placement: "interval", chainage_m: at, start_m: at - 5, end_m: at + 5, options: {}, }); const markView = (lane) => lane.marks().map((m) => [m.structure_id, m.type_id, m.chainage_m]); const stationView = (lane) => lane.stations().map((s) => [s.chainage_m, s.id]); const out = {}; // ① R3 뒤집기 — B06 만 연 채 351 관 → 360 옮김 → 버림 → 카드가 351 관 id 를 물음 → 다시 실음 { const P = "P271-r3"; ids.usePipeIds(P); const profile = fakeProfile(); const lane = createFacilityLane(P, () => profile); lane.setTypes(TYPES); lane.set({ structures: [], pipes: [{ id: "X", chainage_m: 351, facility: "pipe" }] }, false); const first = ids.pipeIdAt(351, P); ids.followMovedPipe(P, 351, 360); // B06 movePipeTo lane.set({ pipes: [{ id: "X", chainage_m: 360, facility: "pipe" }] }); const moved = ids.pipeIdAt(360, P); const ghost = ids.pipeIdAt(351, P); // 버린 뒤 다시 받기 전 카드가 물음(헛 id) lane.set({ pipes: [{ id: "X", chainage_m: 351, facility: "pipe" }] }, false); // 목록 다시 실음 out.r3 = { first, moved, ghostBeforeReload: ghost !== first, after: book(P).ids, afterId: ids.pipeIdAt(351, P), marks: markView(lane), }; } // ② 같은 조작 — B05 먼저 듣는 중(listener 처음부터) 대 B06 먼저(끝에 B05 가 읽음) function scenario(P, listenFromStart) { ids.usePipeIds(P); const profile = fakeProfile(); const lane = createFacilityLane(P, () => profile); const heard = []; const listen = () => lane.onChange(() => heard.push(stationView(lane).length)); if (listenFromStart) listen(); lane.setTypes(TYPES); lane.setStored([wall("w1", 100)]); lane.set( { structures: [wall("w1", 100)], pipes: [ { id: "A", chainage_m: 60, facility: "pipe" }, { id: "B", chainage_m: 200, facility: "pipe" }, ], }, false, ); // B06 폼 — 구조물 옮김 · 관 하나 넣음(새 관 id 없음) · 관 옮김 lane.set({ structures: [wall("w1", 140)] }); lane.set({ pipes: [ { id: "A", chainage_m: 60, facility: "pipe" }, { chainage_m: 120, facility: "pipe" }, { id: "B", chainage_m: 200, facility: "pipe" }, ], }); ids.followMovedPipe(P, 200, 230); lane.set({ pipes: [ { id: "A", chainage_m: 60, facility: "pipe" }, { chainage_m: 120, facility: "pipe" }, { id: "B", chainage_m: 230, facility: "pipe" }, ], }); // 배수유역도가 뒤늦게 같은 목록을 알림(B05 판) lane.set({ pipes: [ { id: "A", chainage_m: 60, facility: "pipe" }, { chainage_m: 120, facility: "pipe" }, { id: "B", chainage_m: 230, facility: "pipe" }, ], }); if (!listenFromStart) listen(); const ledger = book(P).ids; const markIds = lane.marks().map((m) => m.structure_id); return { marks: markView(lane).map(([id, type, at]) => [id.startsWith("pipe-") ? "pipe" : id, type, at]), stations: stationView(lane).map(([at]) => at), minCover: profile.got.minCover, profileMarks: profile.got.structures.length === lane.marks().length, profileStations: JSON.stringify(profile.got.stations) === JSON.stringify(lane.stations()), ghostIds: ledger.filter((entry) => !markIds.includes(entry.id)).length, resets: profile.got.resets, heardB05: listenFromStart ? heard.length : null, }; } out.b05First = scenario("P271-b05", true); out.b06First = scenario("P271-b06", false); // ③ 노선 따라감(가게 씀 + follow) = 편집 안 옮김 · 다시 실음(같은 목록) = 안 옮김 · 측점 다시 안 씀 { const P = "P271-follow"; ids.usePipeIds(P); const profile = fakeProfile(); const lane = createFacilityLane(P, () => profile); const store = useDesignStore(P, null); lane.setTypes(TYPES); lane.set({ structures: [], pipes: [{ id: "A", chainage_m: 80, facility: "pipe" }] }, false); store.set("pipes", [{ id: "A", chainage_m: 95, source: "user", facility: "pipe" }]); store.set("structures", [wall("w9", 150)]); lane.follow(); const followed = { stations: stationView(lane).map(([at]) => at), resets: [...profile.got.resets], }; const writes = profile.got.stationWrites; lane.set({ structures: [wall("w9", 150)] }, false); // 숨었던 B05 가 다시 보이며 같은 목록을 실음 const reshown = { resets: profile.got.resets.length, writes: profile.got.stationWrites - writes }; lane.set({ structures: [wall("w9", 170)] }); // 사용자 옮김 = 옮김 out.follow = { followed, reshown, userMove: profile.got.resets }; } console.log(JSON.stringify(out));