fix(structure): 관 지점 시설 종류 바꾸기 뒤 B06 횡단도 · 알약 · 목록 바로 새 종류(54-4)
- 좌측 패널 종류 바꿈 = 고른 관 지점 실시간 반영 - B06 관 고침이 시설 종류를 받음 · 옛 종류 칸 안 이음 · 알약 · 횡단 다시 그림 - 횡단 상세 읽기에 초안 종류 얹기(짝 산식 common_util_culvert_sets) · 카드 제목 라벨 포함 - 상세 조회가 측점 라벨을 지금 관 지점 종류로(저장 뒤 다시 열기) - 구조물 다시 그림 최신 요청만 채택 - 시험: test_b05_pipe_station_relabel · ui_checks 54-4_종류바꾸기_횡단 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
"""54-4 — 저장으로 관 지점 종류만 바꾸면 횡단 측점 라벨(카드 제목)이 새 종류를 따라감.
|
||||
|
||||
측점은 다시 안 만들어지므로 라벨은 파일에 옛 종류로 남음 — 상세 조회가 지금 관 지점으로 고침.
|
||||
다른 구조물 라벨(옹벽 등)은 건드리지 않음.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_Sections import relabel_pipe_stations, resolve_extra_stations
|
||||
from common_util.common_util_drainage_pipes import parse_pipe_points
|
||||
|
||||
|
||||
def _write_points(root, points):
|
||||
edits = root / "B04_PreProcess" / "drainage" / "edits"
|
||||
edits.mkdir(parents=True, exist_ok=True)
|
||||
(edits / "pipe_points.json").write_text(json.dumps({"points": points}), encoding="utf-8")
|
||||
|
||||
|
||||
def _sections():
|
||||
return [
|
||||
{"chainage_m": 80.0},
|
||||
{"chainage_m": 85.05, "structure": "파형강관 D900"},
|
||||
{"chainage_m": 120.0, "structure": "옹벽"},
|
||||
{"chainage_m": 173.09, "structure": "세월교"},
|
||||
{"chainage_m": 258.12, "structure": "파형강관 D800"},
|
||||
]
|
||||
|
||||
|
||||
def test_relabel_follows_saved_kind(tmp_path):
|
||||
_write_points(
|
||||
tmp_path,
|
||||
[
|
||||
{"chainage_m": 85.05, "source": "spacing", "facility": "box_culvert"},
|
||||
{"chainage_m": 120.0, "source": "user", "facility": "box_culvert"}, # 옹벽 라벨 자리
|
||||
{
|
||||
"chainage_m": 173.09,
|
||||
"source": "stream",
|
||||
"facility": "pipe",
|
||||
"options": {"pipe_kind": "흄관", "pipe_diameter_mm": 1200},
|
||||
},
|
||||
{"chainage_m": 258.12, "source": "spacing", "facility": "ford_pavement"},
|
||||
],
|
||||
)
|
||||
sections = _sections()
|
||||
relabel_pipe_stations(tmp_path, sections)
|
||||
labels = {s["chainage_m"]: s.get("structure") for s in sections}
|
||||
assert labels == {
|
||||
80.0: None,
|
||||
85.05: "BOX암거",
|
||||
120.0: "옹벽", # 관 지점 라벨이 아닌 측점은 그대로
|
||||
173.09: "흄관 D1200",
|
||||
258.12: None, # 물넘이포장 = 라벨 없음(측점 생성 규칙과 같음)
|
||||
}
|
||||
|
||||
|
||||
def test_relabel_matches_generation_rule(tmp_path):
|
||||
points = [
|
||||
{"chainage_m": 85.05, "source": "spacing", "options": {"pipe_diameter_mm": 1000}},
|
||||
{"chainage_m": 173.09, "source": "stream", "facility": "ford_bridge"},
|
||||
]
|
||||
_write_points(tmp_path, points)
|
||||
sections = [
|
||||
{"chainage_m": 85.05, "structure": "세월교"},
|
||||
{"chainage_m": 173.09, "structure": "BOX암거"},
|
||||
]
|
||||
relabel_pipe_stations(tmp_path, sections)
|
||||
generated = dict(resolve_extra_stations(tmp_path, parse_pipe_points(points)))
|
||||
assert [s["structure"] for s in sections] == [generated[85.05], generated[173.09]]
|
||||
|
||||
|
||||
def test_relabel_without_file_keeps_labels(tmp_path):
|
||||
sections = _sections()
|
||||
relabel_pipe_stations(tmp_path, sections)
|
||||
assert sections == _sections()
|
||||
@@ -0,0 +1,211 @@
|
||||
/* 54-4 — 좌측 패널에서 같은 자리 관 지점 시설 종류를 바꿀 때 B06 횡단도 · 종단 알약 · 목록이 바로 새 종류
|
||||
* 시작 화면: B06 횡단설계(#/b06-section) · 검증 프로젝트(sub4_검증) · 로그인 · 목록에 「4+5 배관 1」(85.05 m)
|
||||
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m544 = "단계";`
|
||||
* setup → 처음 관 지점 정본(서버)을 세션 `__544_orig` 에 남김 · 4+5 관 고름 · 종단 도크 폄
|
||||
* cycle → 저장 전 네 종류 서로 바꾸기 12길(배관 → BOX → 세월교 → 물넘이 → 배관 → 세월교 → BOX →
|
||||
* 물넘이 → 세월교 → 배관 → 물넘이 → BOX → 배관) — 길마다 횡단 카드 모양 · 제목 · 목록 · 알약 · 초안 칸
|
||||
* save → BOX암거로 바꾸고 [저장] → 저장 뒤 카드 · 목록 · 알약 = BOX
|
||||
* reopen → (orca reload 뒤) 저장분이 BOX 로 다시 섬 · BOX → 배관 바꾸기도 바로 따라옴(저장 뒤 길)
|
||||
* restore → 처음 정본을 관 초안에 되얹고 [저장] → 서버 관 지점 = 처음과 같음
|
||||
* 긴 단계는 `void` 로 띄우고 `sessionStorage.getItem("__544_out_{단계}")` 로 읽음
|
||||
* 쓰는 것: 검증 프로젝트 관 지점 정본(save · restore) · sessionStorage
|
||||
* 기대 수치: 모든 단계 fails 0 · restore same true
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m544 || "cycle";
|
||||
const raf = window.requestAnimationFrame;
|
||||
window.requestAnimationFrame = (cb) => setTimeout(() => cb(performance.now()), 16);
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const AT = 85.05;
|
||||
const pid = localStorage.getItem("frd_current_project_id");
|
||||
const api = async (u) => (await fetch(u, { credentials: "include" })).json();
|
||||
const txt = (el) => el.innerText.replace(/\s+/g, " ").trim();
|
||||
const NAME = {
|
||||
pipe: "배관",
|
||||
box_culvert: "BOX암거",
|
||||
ford_bridge: "세월교",
|
||||
ford_pavement: "물넘이포장",
|
||||
};
|
||||
const CHIP = {
|
||||
pipe: "배수관",
|
||||
box_culvert: "BOX암거",
|
||||
ford_bridge: "세월교",
|
||||
ford_pavement: null,
|
||||
};
|
||||
/** 그 종류에만 있는 칸 — 다른 종류로 바꾼 뒤 초안에 남으면 옛 칸이 섞인 것. */
|
||||
const OWN = {
|
||||
pipe: ["inlet_revet_form", "outlet_revet_form", "inlet_basin_length_m"], // 세월교도 inlet_type 을 씀(날개벽)
|
||||
box_culvert: ["body_width_m", "body_height_m"],
|
||||
ford_bridge: ["pipe_count"],
|
||||
ford_pavement: [],
|
||||
};
|
||||
const typeSel = () =>
|
||||
document.querySelectorAll(".b05-structure-section .b05-structure__grid select")[1];
|
||||
const rows = () => [...document.querySelectorAll(".b05-route__irregular-list li")];
|
||||
const row = () => rows().find((li) => /^4\+5 (배관|BOX암거|세월교|물넘이포장)/.test(txt(li)));
|
||||
const card = () =>
|
||||
[...document.querySelectorAll(".b06-cross-card")]
|
||||
.map((c) => ({ c, at: Number(txt(c).split(" ")[1]?.replace("m", "")) }))
|
||||
.sort((a, b) => Math.abs(a.at - AT) - Math.abs(b.at - AT))[0]?.c;
|
||||
const pill = () =>
|
||||
[...document.querySelectorAll(".b05-structure__mark")].find(
|
||||
(m) => m.dataset.structureId === `pipe-${AT.toFixed(2)}`,
|
||||
);
|
||||
const draft = () => {
|
||||
const raw = sessionStorage.getItem(`aislo:draft:pipes:${pid}`);
|
||||
const v = raw ? JSON.parse(raw) : null;
|
||||
return (v?.value ?? v ?? []).find((p) => Math.abs(p.chainage_m - AT) < 0.51) ?? null;
|
||||
};
|
||||
const openDock = async () => {
|
||||
if (pill()) return;
|
||||
const handle = document.querySelector(".ui-workflow-overlay__handle--bottom");
|
||||
if (handle?.getAttribute("aria-expanded") !== "true") handle?.click();
|
||||
for (let k = 0; k < 20 && !pill(); k++) await w(300);
|
||||
};
|
||||
const pick = async () => {
|
||||
for (let k = 0; k < 40 && !row(); k++) await w(500);
|
||||
if (!row()?.classList.contains("is-selected")) {
|
||||
row()?.click();
|
||||
await w(2500);
|
||||
}
|
||||
};
|
||||
const until = async (fn, ms = 8000) => {
|
||||
for (let t = 0; t < ms; t += 200) {
|
||||
if (fn()) return true;
|
||||
await w(200);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
/** 횡단 카드 그림이 그 종류 모양인가 — 클래스 · 툴팁(svg title) 첫 낱말로 가름. */
|
||||
const shapeOf = (c) => {
|
||||
const svg = c.querySelector("svg");
|
||||
const cls = [...svg.querySelectorAll("[class]")].map((x) => x.getAttribute("class"));
|
||||
const tips = [...svg.querySelectorAll("title")].map((x) => x.textContent);
|
||||
if (tips.some((t) => t.startsWith("BOX암거"))) return "box_culvert";
|
||||
if (cls.some((k) => k.includes("ford-slab"))) return "ford_bridge";
|
||||
if (cls.some((k) => k.includes("culvert-revet") || k.includes("culvert-pipe"))) return "pipe";
|
||||
return "none";
|
||||
};
|
||||
const check = (to, label) => {
|
||||
const c = card();
|
||||
const got = {
|
||||
shape: c ? shapeOf(c) : "카드 없음",
|
||||
chip: c ? [...c.querySelectorAll(".b06-cross-card__structure")].map(txt) : [],
|
||||
notes: c ? [...c.querySelectorAll(".b06-cross-card__warning")].map(txt) : [],
|
||||
row: row() ? txt(row()) : null,
|
||||
pill: pill() ? `${txt(pill())}|${pill().title}` : null,
|
||||
type: typeSel().value,
|
||||
facility: draft()?.facility,
|
||||
keys: Object.keys(draft()?.options ?? {}),
|
||||
};
|
||||
const want = to === "ford_pavement" ? "none" : to;
|
||||
if (got.shape !== want) fail(`${label}: 모양 ${got.shape} ≠ ${want}`);
|
||||
if (CHIP[to] ? got.chip.join() !== CHIP[to] : got.chip.length)
|
||||
fail(`${label}: 제목 ${got.chip}`);
|
||||
if (to === "ford_pavement" && !got.notes.some((n) => n.includes("물넘이포장")))
|
||||
fail(`${label}: 물넘이 알림 없음 ${got.notes}`);
|
||||
if (got.notes.some((n) => Object.values(NAME).some((v) => v !== NAME[to] && n.includes(v))))
|
||||
fail(`${label}: 옛 종류 알림 ${got.notes}`);
|
||||
if (!got.row?.startsWith(`4+5 ${NAME[to]}`)) fail(`${label}: 목록 ${got.row}`);
|
||||
if (!got.pill || !got.pill.includes(NAME[to])) fail(`${label}: 알약 ${got.pill}`);
|
||||
if (got.type !== to) fail(`${label}: 폼 종류 ${got.type}`);
|
||||
if (!["setup", "save", "reopen"].includes(MODE) && got.facility !== to)
|
||||
fail(`${label}: 초안 종류 ${got.facility}`);
|
||||
for (const [kind, keys] of Object.entries(OWN))
|
||||
if (kind !== to && keys.some((k) => got.keys.includes(k)))
|
||||
fail(`${label}: 옛 ${kind} 칸 섞임 ${got.keys.filter((k) => keys.includes(k))}`);
|
||||
return got;
|
||||
};
|
||||
const change = async (to) => {
|
||||
const t = typeSel();
|
||||
t.value = to;
|
||||
t.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await until(() => card() && shapeOf(card()) === (to === "ford_pavement" ? "none" : to));
|
||||
await w(600);
|
||||
};
|
||||
const save = async () => {
|
||||
[...document.querySelectorAll("button")].find((b) => b.innerText.trim() === "저장").click();
|
||||
await until(() => document.querySelector(".ui-toast, [class*='toast']"), 60000);
|
||||
await w(4000);
|
||||
return document.querySelector(".ui-toast, [class*='toast']")?.innerText ?? null;
|
||||
};
|
||||
const out = { mode: MODE, fails: [] };
|
||||
const fail = (what) => out.fails.push(what);
|
||||
|
||||
if (MODE === "setup") {
|
||||
const pipes = (await api(`/api/projects/${pid}/drainage/pipe-points`)).pipe_points;
|
||||
sessionStorage.setItem("__544_orig", JSON.stringify(pipes));
|
||||
out.orig = pipes.find((p) => Math.abs(p.chainage_m - AT) < 0.51);
|
||||
await pick();
|
||||
await openDock();
|
||||
out.start = check(typeSel().value, "처음");
|
||||
} else if (MODE === "cycle") {
|
||||
await pick();
|
||||
await openDock();
|
||||
const path = [
|
||||
"pipe",
|
||||
"box_culvert",
|
||||
"ford_bridge",
|
||||
"ford_pavement",
|
||||
"pipe",
|
||||
"ford_bridge",
|
||||
"box_culvert",
|
||||
"ford_pavement",
|
||||
"ford_bridge",
|
||||
"pipe",
|
||||
"ford_pavement",
|
||||
"box_culvert",
|
||||
"pipe",
|
||||
];
|
||||
if (typeSel().value !== path[0]) fail(`처음 종류 ${typeSel().value}`);
|
||||
out.steps = [];
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const label = `${i} ${path[i - 1]}→${path[i]}`;
|
||||
await change(path[i]);
|
||||
const got = check(path[i], label);
|
||||
out.steps.push(
|
||||
`${label} ${got.shape} ${got.chip.join("/") || "-"} | ${got.row} | ${got.pill}`,
|
||||
);
|
||||
}
|
||||
out.ways = new Set(path.slice(1).map((to, i) => `${path[i]}→${to}`)).size;
|
||||
} else if (MODE === "save") {
|
||||
await pick();
|
||||
await openDock();
|
||||
await change("box_culvert");
|
||||
out.toast = await save();
|
||||
await openDock();
|
||||
out.after = check("box_culvert", "저장 뒤");
|
||||
const saved = (await api(`/api/projects/${pid}/drainage/pipe-points`)).pipe_points;
|
||||
out.server = saved.find((p) => Math.abs(p.chainage_m - AT) < 0.51)?.facility;
|
||||
if (out.server !== "box_culvert") fail(`서버 종류 ${out.server}`);
|
||||
} else if (MODE === "reopen") {
|
||||
await pick();
|
||||
await openDock();
|
||||
out.reopened = check("box_culvert", "다시 연 BOX");
|
||||
await change("pipe");
|
||||
out.toPipe = check("pipe", "저장 뒤 BOX→배관");
|
||||
} else if (MODE === "restore") {
|
||||
const orig = JSON.parse(sessionStorage.getItem("__544_orig") ?? "null");
|
||||
if (!orig) fail("처음 정본 없음 — setup 먼저");
|
||||
else {
|
||||
const points = orig.map(({ lonlat, ...rest }) => rest); // 좌표는 서버가 다시 계산(DetailPipeInput)
|
||||
sessionStorage.setItem(`aislo:draft:pipes:${pid}`, JSON.stringify(points));
|
||||
location.reload();
|
||||
return "reload — 다시 부르면 저장"; // 새로 고친 뒤 `__m544 = "restore2"`
|
||||
}
|
||||
} else if (MODE === "restore2") {
|
||||
await pick();
|
||||
out.toast = await save();
|
||||
const orig = JSON.parse(sessionStorage.getItem("__544_orig"));
|
||||
const now = (await api(`/api/projects/${pid}/drainage/pipe-points`)).pipe_points;
|
||||
const pick2 = (list) =>
|
||||
list.map((p) => [p.chainage_m, p.facility ?? "pipe", JSON.stringify(p.options ?? {})]);
|
||||
out.same = JSON.stringify(pick2(now)) === JSON.stringify(pick2(orig));
|
||||
if (!out.same) fail("되돌린 정본이 처음과 다름");
|
||||
out.at = now.find((p) => Math.abs(p.chainage_m - AT) < 0.51);
|
||||
}
|
||||
window.requestAnimationFrame = raf;
|
||||
out.failCount = out.fails.length;
|
||||
sessionStorage.setItem(`__544_out_${MODE}`, JSON.stringify(out));
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user