Merge remote-tracking branch 'origin/sub_laptop_4' into main_laptop_1
This commit is contained in:
@@ -118,6 +118,8 @@ export const STATE_REGISTRY = {
|
||||
},
|
||||
/** 설계 가게(`design/design_store`)의 고른 측점 · 구조물 — `{chainageM, structureId}`. */
|
||||
selection: { bucket: "draft", scope: "project" },
|
||||
/** 구조물 패널 종류별 캐시 — 고른 구조물 하나 × 종류마다 옛 값(54-32). [저장]·[확정] 뒤 비움. */
|
||||
"structure-type-cache": { bucket: "draft", scope: "project" },
|
||||
/** 배수관·암거 조정창에서 예약한 옵션 값. */
|
||||
culvertopt: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:culvertopt:${p}:${r}` },
|
||||
/** 배수관 이동(측점 옮김) 예약. */
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
pendingAlignment,
|
||||
} from "../../B05_Profile/B05_Profile_UI_Profile_Edit";
|
||||
import { flushCulvertOptions } from "../../B06_Section/B06_Section_Api_Culvert_Options";
|
||||
import { stateKey } from "../b_page_state";
|
||||
import { clearState, stateKey } from "../b_page_state";
|
||||
|
||||
/** 저장 차례 — 이 순서 그대로 돈다. */
|
||||
export const DESIGN_SAVE_ORDER = [
|
||||
@@ -134,6 +134,7 @@ export async function saveDesign(
|
||||
const label = TOLERANT[step];
|
||||
await (label ? run().catch(reportOnly(label)) : run());
|
||||
}
|
||||
clearState("structure-type-cache", ctx.projectId); // 종류별 캐시는 저장 뒤 비움(54-32)
|
||||
await ctx.done?.(confirm);
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
||||
@@ -474,6 +474,7 @@ export function createStructuresSection(
|
||||
endFields,
|
||||
facilityOptions,
|
||||
callbacks,
|
||||
projectId: sectionOptions.projectId ?? null,
|
||||
structures,
|
||||
types: () => types,
|
||||
typeMap,
|
||||
|
||||
@@ -10,10 +10,16 @@
|
||||
|
||||
import type { StructureInstance, StructureType } from "./B05_Profile_Api_Structures";
|
||||
import type { StructuresCommitContext } from "./B05_Profile_UI_Structures_Panel_Commit";
|
||||
import { asRoad } from "./B05_Profile_UI_Structures_Define";
|
||||
import { readState, writeState } from "../A00_Common/b_page_state";
|
||||
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
|
||||
import { readGroupItems, readSummaryItem } from "../common_util/common_util_summary_item";
|
||||
|
||||
/** 배선에 필요한 패널 내부 값 — 이름은 분리 전 지역변수와 같다. */
|
||||
/** 배선에 필요한 값 — 저장 쪽(`StructuresCommitContext`)과 겹치는 것은 그대로 물려받는다. */
|
||||
export interface StructuresEventContext extends StructuresCommitContext {
|
||||
/** 종류별 캐시(54-32) 세션 키의 프로젝트. */
|
||||
projectId: string | null;
|
||||
typeSelect: HTMLSelectElement;
|
||||
primary: HTMLButtonElement;
|
||||
removeButton: HTMLButtonElement;
|
||||
@@ -31,34 +37,103 @@ export interface StructuresEventContext extends StructuresCommitContext {
|
||||
}
|
||||
|
||||
export function bindStructuresEvents(ctx: StructuresEventContext): void {
|
||||
/** 새로 놓기(54-32) — 고름 · 측점 · 강조를 풀고 군(· 종류)만 남긴 빈 기본값 폼. */
|
||||
function startNew(group: string, typeId?: string): void {
|
||||
ctx.resetForm();
|
||||
ctx.groupSelect.value = group;
|
||||
ctx.syncTypeOptions(typeId);
|
||||
}
|
||||
ctx.groupSelect.addEventListener("change", () => {
|
||||
ctx.cancelTempPipe(); // 종류가 달라진다 — 임시 배치는 취소(2026-08-18).
|
||||
ctx.syncTypeOptions();
|
||||
// 종류가 바뀌면 기존 항목 수정이 아니라 새 항목 추가로 넘어간다(옵션 스키마가 달라진다).
|
||||
if (ctx.editingId()) {
|
||||
ctx.setEditingId(null);
|
||||
ctx.syncButtons();
|
||||
ctx.renderList();
|
||||
ctx.callbacks.onSelect(null);
|
||||
}
|
||||
// 고른 항목에서 군을 바꿈 = 새 항목 추가(54-32) — 고른 항목 값은 그대로
|
||||
if (ctx.editingId() || ctx.selectedPipeChainage() !== null) startNew(ctx.groupSelect.value);
|
||||
else ctx.syncTypeOptions();
|
||||
});
|
||||
ctx.typeSelect.addEventListener("change", () => {
|
||||
ctx.cancelTempPipe(); // 종류가 달라진다 — 임시 배치는 취소(2026-08-18).
|
||||
if (ctx.editingId() || ctx.selectedPipeChainage() !== null) return switchSelectedType();
|
||||
ctx.syncPlacementFields();
|
||||
ctx.renderOptionFields();
|
||||
// 배수관·BOX암거·세월교·독립 기슭막이는 부속 옵션 서브폼이 따라 열려야 한다 —
|
||||
// 수동 추가든 자동 배치 지점 편집이든 같은 옵션을 받는다(2026-08-17 사용자 지시:
|
||||
// 자동은 통수단면으로 자리를 잡아 준 것일 뿐 같은 구조물이다).
|
||||
ctx.syncFacilityForm();
|
||||
// 고른 관 지점의 시설 종류를 바꿈(54-4) — 실시간 반영으로 목록 · 알약 · 횡단도가 바로 따라옴
|
||||
if (ctx.selectedPipeChainage() !== null && ctx.currentType()?.managed_by) ctx.liveCommit();
|
||||
if (ctx.editingId()) {
|
||||
ctx.setEditingId(null);
|
||||
ctx.syncButtons();
|
||||
ctx.renderList();
|
||||
ctx.callbacks.onSelect(null);
|
||||
}
|
||||
});
|
||||
|
||||
/** 고른 항목의 종류를 같은 군 안에서 바꿈(54-32) — 그 항목이 새 종류가 됨. 옛 종류 값은
|
||||
* 종류별 캐시에 담고, 담아 둔 종류로 돌아오면 그 값으로 · 처음이면 새 종류 기본값으로.
|
||||
* 관 지점 시설(배관 ↔ 박스 ↔ 세월교 ↔ 물넘이)도 같은 규칙 · 실시간 반영은 54-4 길 그대로. */
|
||||
function switchSelectedType(): void {
|
||||
const next = ctx.currentType();
|
||||
const id = ctx.editingId();
|
||||
const pipe = ctx.selectedPipeChainage();
|
||||
const index = id ? ctx.structures.findIndex((entry) => entry.structure_id === id) : -1;
|
||||
const record = index >= 0 ? ctx.structures[index] : null;
|
||||
const pipeItem =
|
||||
pipe === null
|
||||
? null
|
||||
: ctx.pipeFacilities().find((entry) => Math.abs(entry.chainage_m - pipe) < PIPE_MATCH_M);
|
||||
const prev = ctx.typeMap().get(record ? asRoad(record).type_id : (pipeItem?.facility ?? ""));
|
||||
// 정본이 다른 종류(관 지점 ↔ 구조물)로 · 빈칸으로 = 새로 놓기
|
||||
if (!next || !prev || !!next.managed_by !== !!prev.managed_by)
|
||||
return startNew(ctx.groupSelect.value, next?.type_id);
|
||||
const key = record ? `s:${id}` : `p:${pipe}`;
|
||||
// 구조물은 정본 기록(안 보이는 상세 칸까지) · 관은 아직 옛 종류로 그려진 폼에서 읽음
|
||||
stashTypeValues(
|
||||
key,
|
||||
prev.type_id,
|
||||
record
|
||||
? {
|
||||
options: record.options,
|
||||
summary_item: record.summary_item,
|
||||
group_items: record.group_items,
|
||||
}
|
||||
: {
|
||||
options: { ...ctx.facilityOptions.readOptions(), ...ctx.groupBoxes.shapes() },
|
||||
summary_item: ctx.summary(),
|
||||
group_items: ctx.groupBoxes.get(),
|
||||
},
|
||||
);
|
||||
const back = recallTypeValues(key, next.type_id);
|
||||
// 기록을 새 종류로 갈아 둠 — 되살린 값의 안 보이는 칸이 [수정] 병합에서 살게
|
||||
const draft = record && { ...record, type_id: next.type_id, options: back?.options ?? {} };
|
||||
if (draft) ctx.structures[index] = draft;
|
||||
ctx.syncPlacementFields();
|
||||
ctx.renderOptionFields(back?.options);
|
||||
ctx.setSummary(readSummaryItem(back?.summary_item));
|
||||
ctx.groupBoxes.set(readGroupItems(back?.group_items));
|
||||
ctx.groupBoxes.setShapes(back?.options ?? {});
|
||||
ctx.syncFacilityForm(back?.options);
|
||||
ctx.liveCommit();
|
||||
// 반영이 막혀(측점 등) 기록이 그대로면 옛 기록으로 되돌림
|
||||
if (record && draft && ctx.structures[index] === draft) ctx.structures[index] = record;
|
||||
}
|
||||
|
||||
type TypeValues = {
|
||||
options?: Record<string, string | number>;
|
||||
summary_item?: unknown;
|
||||
group_items?: unknown;
|
||||
};
|
||||
type TypeCache = Record<string, Record<string, TypeValues>>;
|
||||
/** 관 지점은 재계산 스냅으로 누가거리가 살짝 옮겨짐 — 관 매칭 기준 안이면 같은 관. */
|
||||
function cacheKeyOf(cache: TypeCache, key: string): string {
|
||||
if (!key.startsWith("p:")) return key;
|
||||
const at = Number(key.slice(2));
|
||||
const hit = Object.keys(cache).find(
|
||||
(k) => k.startsWith("p:") && Math.abs(Number(k.slice(2)) - at) < PIPE_MATCH_M,
|
||||
);
|
||||
return hit ?? key;
|
||||
}
|
||||
function stashTypeValues(key: string, typeId: string, values: TypeValues): void {
|
||||
const cache = readState<TypeCache>("structure-type-cache", ctx.projectId) ?? {};
|
||||
const slot = cacheKeyOf(cache, key);
|
||||
cache[slot] = { ...cache[slot], [typeId]: values };
|
||||
writeState("structure-type-cache", cache, ctx.projectId);
|
||||
}
|
||||
function recallTypeValues(key: string, typeId: string): TypeValues | null {
|
||||
const cache = readState<TypeCache>("structure-type-cache", ctx.projectId) ?? {};
|
||||
return cache[cacheKeyOf(cache, key)]?.[typeId] ?? null;
|
||||
}
|
||||
ctx.primary.addEventListener("click", () => void ctx.commit());
|
||||
ctx.removeButton.addEventListener("click", () => {
|
||||
// 임시 배치 중이면 취소와 같다 — 관을 물리고 추가 직전 상태로(2026-08-18).
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/* 54-32 고른 구조물에서 군 바꿈 = 새로 놓기 · 같은 군 종류 바꿈 = 새 종류 기본값 + 종류별 캐시
|
||||
* 시작 화면: B06 횡단설계(#/b06-section) · 검증 프로젝트(sub4) · 로그인
|
||||
* 부르는 법: 단계마다 `window.__m5432 = "단계";` 뒤 이 파일 본문을 eval
|
||||
* a (B06) 돌쌓기(찰) 1: 칸 고침 → 다른 종류 → 돌아옴 = 고친 값 · 군 바꿈 = 고름 0
|
||||
* 4+5 배관 1: 관경 고침 → BOX(칸 고침) → 배관 = 고친 관경 · 군 바꿈 = 고름 0 · 캐시 있음
|
||||
* b (B05) 배관 → BOX = B06 에서 고친 BOX 값(캐시 공유) → 배관 · 관경 · 돌쌓기 칸 원래 값으로
|
||||
* c [저장] → 캐시 비움 · 배관 → BOX = BOX 기본 · → 배관 = 저장 관경 · 다시 [저장]
|
||||
* 결과는 sessionStorage `__5432_out_{단계}` · 기대: fails 0
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m5432 || "a";
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const pid = localStorage.getItem("frd_current_project_id");
|
||||
const t = (e) => e.innerText.replace(/\s+/g, " ").trim();
|
||||
const fails = [];
|
||||
const out = {};
|
||||
const fail = (m) => fails.push(m);
|
||||
const sec = () => document.querySelector(".b05-structure-section");
|
||||
const grid = () => sec().querySelectorAll(".b05-structure__grid select");
|
||||
const rows = () => [...document.querySelectorAll(".b05-route__irregular-list li")];
|
||||
const row = (head) => rows().find((li) => t(li).startsWith(head));
|
||||
const picked = () => rows().filter((li) => li.classList.contains("is-selected")).length;
|
||||
const read = (name) => {
|
||||
const raw = sessionStorage.getItem(`aislo:draft:${name}:${pid}`);
|
||||
const v = raw ? JSON.parse(raw) : null;
|
||||
return v?.value ?? v;
|
||||
};
|
||||
const pipe = () => (read("pipes") ?? []).find((p) => Math.abs(p.chainage_m - 85.05) < 0.51);
|
||||
const fire = (el, value) => {
|
||||
el.value = value;
|
||||
el.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
};
|
||||
const pick = async (head) => {
|
||||
for (let k = 0; k < 40 && !row(head); k++) await w(300);
|
||||
if (!row(head)?.classList.contains("is-selected")) row(head)?.click();
|
||||
await w(2000);
|
||||
};
|
||||
const setType = async (id) => (fire(grid()[1], id), await w(2500));
|
||||
const setGroup = async (g) => (fire(grid()[0], g), await w(2000));
|
||||
/** 종류 칸 아래 첫 고를 수 있는 선택 칸(관경 · BOX 폭 등) */
|
||||
const firstSelect = () =>
|
||||
[...sec().querySelectorAll("select")]
|
||||
.slice(2)
|
||||
.find(
|
||||
(s) => s.offsetParent && !s.disabled && [...s.options].filter((o) => o.value).length > 1,
|
||||
);
|
||||
const other = (s) => [...s.options].map((o) => o.value).find((v) => v && v !== s.value);
|
||||
const firstNumber = () =>
|
||||
[...sec().querySelectorAll("input")].find(
|
||||
(i) =>
|
||||
i.offsetParent &&
|
||||
!i.disabled &&
|
||||
i.value !== "" &&
|
||||
!isNaN(Number(i.value)) &&
|
||||
!/측점/.test(t(i.closest("label") ?? i)),
|
||||
);
|
||||
const S = "4+0~4+10 돌쌓기(찰)";
|
||||
const P = "4+5 배관";
|
||||
const ck = () => sessionStorage.getItem(`aislo:draft:structure-type-cache:${pid}`);
|
||||
const memo = JSON.parse(sessionStorage.getItem("__5432_memo") || "{}");
|
||||
|
||||
if (MODE === "a") {
|
||||
await pick(S);
|
||||
const t0 = grid()[1].value;
|
||||
const num = firstNumber();
|
||||
memo.sLabel = t(num.closest("label"));
|
||||
memo.s0 = num.value;
|
||||
const s1 = String(Number(num.value) + 0.5);
|
||||
fire(num, s1);
|
||||
await w(1200);
|
||||
const t1 = [...grid()[1].options].map((o) => o.value).find((v) => v && v !== t0);
|
||||
await setType(t1);
|
||||
out.afterSwitch = {
|
||||
type: grid()[1].value,
|
||||
picked: picked(),
|
||||
primary: t(sec().querySelector("button")),
|
||||
};
|
||||
if (picked() !== 1) fail(`종류 바꿈 뒤 고름 ${picked()}`);
|
||||
await setType(t0);
|
||||
const back = firstNumber();
|
||||
out.structBack = back?.value;
|
||||
if (Number(back?.value) !== Number(s1)) fail(`돌쌓기 되살림 ${back?.value} ≠ ${s1}`);
|
||||
fire(back, memo.s0);
|
||||
await w(1200);
|
||||
await setGroup("B");
|
||||
out.structGroup = { picked: picked(), type: grid()[1].value, row: !!row(S) };
|
||||
if (picked() !== 0 || grid()[1].value !== "")
|
||||
fail(`돌쌓기 군 바꿈 ${JSON.stringify(out.structGroup)}`);
|
||||
|
||||
await pick(P);
|
||||
const d = firstSelect();
|
||||
memo.p0 = d.value;
|
||||
memo.p1 = other(d);
|
||||
fire(d, memo.p1);
|
||||
await w(2500);
|
||||
await setType("box_culvert");
|
||||
out.boxType = pipe()?.facility;
|
||||
if (pipe()?.facility !== "box_culvert") fail(`BOX 초안 ${pipe()?.facility}`);
|
||||
const b = firstSelect();
|
||||
memo.bDefault = b.value;
|
||||
memo.b1 = other(b);
|
||||
fire(b, memo.b1);
|
||||
await w(2500);
|
||||
await setType("pipe");
|
||||
out.pipeBack = firstSelect()?.value;
|
||||
if (firstSelect()?.value !== memo.p1) fail(`배관 되살림 ${firstSelect()?.value} ≠ ${memo.p1}`);
|
||||
await setGroup("C");
|
||||
out.pipeGroup = { picked: picked(), type: grid()[1].value, facility: pipe()?.facility };
|
||||
if (picked() !== 0 || pipe()?.facility !== "pipe")
|
||||
fail(`배관 군 바꿈 ${JSON.stringify(out.pipeGroup)}`);
|
||||
out.cache = !!ck();
|
||||
if (!ck()) fail("캐시 없음");
|
||||
}
|
||||
|
||||
if (MODE === "b") {
|
||||
await pick(P);
|
||||
await setType("box_culvert");
|
||||
out.boxFromCache = firstSelect()?.value;
|
||||
if (firstSelect()?.value !== memo.b1) fail(`B05 BOX 캐시 ${firstSelect()?.value} ≠ ${memo.b1}`);
|
||||
await setType("pipe");
|
||||
out.pipeFromCache = firstSelect()?.value;
|
||||
if (firstSelect()?.value !== memo.p1)
|
||||
fail(`B05 배관 캐시 ${firstSelect()?.value} ≠ ${memo.p1}`);
|
||||
fire(firstSelect(), memo.p0);
|
||||
await w(2500);
|
||||
out.pipeRestored = pipe()?.options?.diameter_mm ?? firstSelect()?.value;
|
||||
}
|
||||
|
||||
if (MODE === "c") {
|
||||
const save = async () => {
|
||||
[...document.querySelectorAll("button")].find((x) => t(x) === "저장").click();
|
||||
await w(15000);
|
||||
};
|
||||
await save();
|
||||
out.cacheAfterSave = ck();
|
||||
if (ck()) fail("저장 뒤 캐시 남음");
|
||||
await pick(P);
|
||||
await setType("box_culvert");
|
||||
out.boxAfterSave = firstSelect()?.value;
|
||||
if (firstSelect()?.value !== memo.bDefault)
|
||||
fail(`저장 뒤 BOX ${firstSelect()?.value} ≠ 기본 ${memo.bDefault}`);
|
||||
await setType("pipe");
|
||||
out.pipeAfterSave = firstSelect()?.value;
|
||||
if (firstSelect()?.value !== memo.p0) fail(`저장 뒤 배관 ${firstSelect()?.value} ≠ ${memo.p0}`);
|
||||
await save();
|
||||
out.cacheEnd = ck();
|
||||
}
|
||||
|
||||
sessionStorage.setItem("__5432_memo", JSON.stringify(memo));
|
||||
sessionStorage.setItem(`__5432_out_${MODE}`, JSON.stringify({ fails, out, memo }));
|
||||
})();
|
||||
Reference in New Issue
Block a user