Merge remote-tracking branch 'origin/sub_laptop_1' into main_laptop_1
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
"""PLAN 54-26 — 사용자 구조물 표 09 사면안정 › 흙막이 · 옹벽 · 소단.
|
||||
|
||||
· 컨테이너 = 흙막이 「흙막이」 · 옹벽 「옹벽」 · 소단 「상세 내용」 — 칸은 표대로만 · 표 밖 옛 칸은 숨김(값 · 기본값 그대로)
|
||||
· 흙막이 형태 = 배관 흙막이 6 · 옹벽 형태 = 콘크리트 하나(새 칸 `wall_form` · 옛 형식 `form` 값은 그대로 통과)
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from B05_Profile.B05_Profile_Structures_Repository import _validate_types
|
||||
from B05_Profile.B05_Profile_Structures_Schema import (
|
||||
DEFINITION_PATH,
|
||||
StructureInstance,
|
||||
load_structure_types,
|
||||
)
|
||||
from M02_MasterTemplete.StructOptions import M02_SO_Definition
|
||||
from M02_MasterTemplete.StructOptions.M02_SO_Groups import groups_of
|
||||
from M02_MasterTemplete.StructOptions.M02_SO_Options import hidden_keys
|
||||
|
||||
TABLE = {
|
||||
"soil_guard": (
|
||||
"gsoilgdetail",
|
||||
"흙막이",
|
||||
["form", "length_m", "height_m", "before_m", "after_m"],
|
||||
),
|
||||
"retaining_wall": (
|
||||
"gretaindetail",
|
||||
"옹벽",
|
||||
["wall_form", "length_m", "height_m", "before_m", "after_m"],
|
||||
),
|
||||
"berm": ("gbermdetail", "상세 내용", ["width_m", "height_m"]),
|
||||
}
|
||||
SOIL_FORMS = ["돌쌓기(찰)", "돌쌓기(메)", "콘크리트", "돌망태", "통나무·목재틀", "바자"]
|
||||
|
||||
|
||||
def _types():
|
||||
return {t.type_id: t for t in load_structure_types()}
|
||||
|
||||
|
||||
def test_컨테이너_하나_칸은_표대로_차례대로():
|
||||
groups = groups_of()
|
||||
types = _types()
|
||||
hidden = hidden_keys()
|
||||
for type_id, (gid, name, keys) in TABLE.items():
|
||||
assert groups[type_id] == [{"id": gid, "name": name, "options": keys}]
|
||||
options = [o.key for o in types[type_id].options]
|
||||
assert options[: len(keys)] == keys # 칸 차례 = 표 차례
|
||||
assert hidden[type_id] == set(options) - set(keys) # 표 밖 = 숨김(값은 남음)
|
||||
|
||||
|
||||
def test_형태_선택지():
|
||||
types = _types()
|
||||
soil = {o.key: o for o in types["soil_guard"].options}
|
||||
pipe = {o.key: o for o in types["pipe"].options}
|
||||
assert soil["form"].choices == SOIL_FORMS == pipe["outlet_revet_form"].choices
|
||||
assert soil["form"].phase == "b05" # B05 = B06
|
||||
wall = {o.key: o for o in types["retaining_wall"].options}
|
||||
assert wall["wall_form"].choices == ["콘크리트"]
|
||||
assert "반중력식" in wall["form"].choices # 옛 형식 값 그대로 통과
|
||||
doc = json.loads(Path(DEFINITION_PATH).read_text("utf-8"))
|
||||
assert doc["종류"]["soil_guard"]["이름"]["form=통나무·목재틀"] == "통나무/목재틀"
|
||||
|
||||
|
||||
def test_옛_값_저장_막지_않음():
|
||||
def item(type_id, **options):
|
||||
return StructureInstance(
|
||||
type_id=type_id,
|
||||
placement="interval",
|
||||
chainage_m=20,
|
||||
start_m=20,
|
||||
end_m=30,
|
||||
options={"length_m": 10, "before_m": 0, "after_m": 10, **options},
|
||||
)
|
||||
|
||||
_validate_types(
|
||||
[
|
||||
item("soil_guard", height_m=2), # 형태 없는 옛 흙막이
|
||||
item("retaining_wall", form="반중력식", height_m=2),
|
||||
item("berm", width_m=0.5, interval_m=3, height_m=5),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_정의_검사_통과_판_이력():
|
||||
doc = json.loads(Path(DEFINITION_PATH).read_text("utf-8"))
|
||||
assert M02_SO_Definition.check(doc) == []
|
||||
assert M02_SO_Definition.history_path(doc["판"] - 1).is_file()
|
||||
@@ -20,6 +20,7 @@ KINDS = {
|
||||
"boulder_masonry": "gboulderdetail",
|
||||
"berm": "gbermdetail",
|
||||
}
|
||||
TABLE_09 = {"soil_guard", "retaining_wall", "berm"}
|
||||
|
||||
|
||||
def test_C군_종류마다_컨테이너_하나_옵션_전부():
|
||||
@@ -27,6 +28,8 @@ def test_C군_종류마다_컨테이너_하나_옵션_전부():
|
||||
assert sorted(t.type_id for t in types) == sorted(KINDS)
|
||||
groups = groups_of()
|
||||
for item in types:
|
||||
if item.type_id in TABLE_09: # 표 09 로 바뀜 — test_54_26_slope_table09
|
||||
continue
|
||||
assert groups[item.type_id] == [
|
||||
{
|
||||
"id": KINDS[item.type_id],
|
||||
@@ -55,4 +58,4 @@ def test_잇기는_컨테이너마다_집계_한_줄():
|
||||
]
|
||||
)
|
||||
assert [r["복사본"] for r in rows] == ["s1", "s1__gretaindetail"]
|
||||
assert rows[1]["summary_item"] == link and rows[1]["그룹"] == "상세 내용"
|
||||
assert rows[1]["summary_item"] == link and rows[1]["그룹"] == "옹벽" # 표 09 컨테이너 이름
|
||||
|
||||
@@ -270,7 +270,9 @@ def test_slope_group_computes_range_from_length():
|
||||
assert options[key].default == default, f"{type_id}.{key} 기본값"
|
||||
assert options[key].input == "number" and options[key].unit == "m", key
|
||||
keys = [option.key for option in item.options if option.phase == "b05"]
|
||||
assert keys[:3] == ["height_m", "length_m", "before_m"], f"{type_id} 옵션 순서"
|
||||
# 표 09 흙막이 · 옹벽 = 형태 → 길이 → 높이 → 전 → 후(54-26 · test_54_26_slope_table09)
|
||||
if type_id not in ("soil_guard", "retaining_wall"):
|
||||
assert keys[:3] == ["height_m", "length_m", "before_m"], f"{type_id} 옵션 순서"
|
||||
|
||||
|
||||
def test_retaining_wall_renamed_plain():
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
/* 54-26 — B05 · B06 좌측 「구조물 배치」 사면안정 표 09(흙막이 · 옹벽 · 소단)
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(랩탑_보조)
|
||||
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m5426 = "단계";` 를 붙여
|
||||
* seed → 원래 목록을 sessionStorage 에 두고 흙막이(20+0 · 잇기 하나) · 소단을 심음(PUT) · 그 뒤 orca reload
|
||||
* check → 세 종류 컨테이너 1(이름) · 칸 = 표대로 · 밖 칸 0 · 잇기 1 · 램프 · 형태 선택지 6/1
|
||||
* B05 에서 한 번(값을 sessionStorage 에 둠) · B06(#/b06-section) 에서 한 번(B05 = B06)
|
||||
* split → 흙막이 길이 10 · 전 3 → 후 7(B05 · 실시간 반영 · 저장 안 함)
|
||||
* height → B06 — 좌측 높이 1.8 · 2.3 → 20+0 횡단 벽 그림 높이(H=) 같은 값
|
||||
* save → [저장] — 파일 = 형태 · 길이 10 · 전 3 · 후 7 · 높이 2.3 · 그 뒤 orca reload
|
||||
* verify → 다시 연 흙막이 칸 = 파일 값(B05 · B06 각각)
|
||||
* restore → 원래 목록으로 되돌림(PUT) · 초안 비움
|
||||
* 쓰는 것: 검증 프로젝트 structures.json(되돌림 단계가 원래 값으로 다시 씀)
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m5426 || "check";
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const until = async (fn, ms = 20000) => {
|
||||
for (let t = 0; t < ms; t += 200) {
|
||||
if (fn()) return true;
|
||||
await w(200);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const projectId = localStorage.getItem("frd_current_project_id");
|
||||
const modUrl = (part) =>
|
||||
performance
|
||||
.getEntriesByType("resource")
|
||||
.map((e) => e.name)
|
||||
.filter((n) => n.includes(part))
|
||||
.pop();
|
||||
const api = await import(modUrl("/B05_Profile/B05_Profile_Api_Structures.ts"));
|
||||
const LINK = { layer: "system", owner: null, key: "SS000001", name: "54-26 잇기" };
|
||||
const KINDS = {
|
||||
soil_guard: ["gsoilgdetail", "흙막이", /흙막이$/, 6], // 심은 것 하나 · 전 3 이면 19+17~20+7
|
||||
retaining_wall: ["gretaindetail", "옹벽", /^10\+0~10\+10옹벽/, 1],
|
||||
berm: ["gbermdetail", "상세 내용", /^28\+0~28\+10소단/, 0],
|
||||
};
|
||||
const TABLE = {
|
||||
soil_guard: ["form", "length_m", "height_m", "before_m", "after_m"],
|
||||
retaining_wall: ["wall_form", "length_m", "height_m", "before_m", "after_m"],
|
||||
berm: ["width_m", "height_m"],
|
||||
};
|
||||
const section = () => document.querySelector(".b05-structure-section");
|
||||
const selects = () => section().querySelectorAll(".b05-structure__grid select");
|
||||
const visible = (n) => n.offsetParent !== null;
|
||||
const listItem = (re) =>
|
||||
[...document.querySelectorAll(".b05-route__irregular-list li")].find((l) =>
|
||||
re.test(l.textContent.trim()),
|
||||
);
|
||||
const open = async (re) => {
|
||||
await until(() => listItem(re));
|
||||
const li = listItem(re);
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(800);
|
||||
if (!selects()[1].value) (li.querySelector("button") ?? li).click(); // 재선택 = 해제 — 한 번 더
|
||||
await until(() => !!selects()[1].value);
|
||||
await w(400);
|
||||
};
|
||||
const reset = async () => {
|
||||
section().querySelector(".b05-route__irregular-actions button:last-child").click();
|
||||
await w(400);
|
||||
};
|
||||
const field = (key) =>
|
||||
section().querySelector(
|
||||
`[data-option-keys="${key}"] input, [data-option-keys="${key}"] select`,
|
||||
);
|
||||
const set = async (key, value) => {
|
||||
const input = field(key);
|
||||
input.value = String(value);
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await w(500);
|
||||
};
|
||||
const shape = (type) => {
|
||||
const boxes = [...section().querySelectorAll("[data-b05-group]")].filter(visible);
|
||||
const box = boxes[0];
|
||||
const form = box?.querySelector(
|
||||
'[data-option-keys="form"] select, [data-option-keys="wall_form"] select',
|
||||
);
|
||||
return {
|
||||
boxes: boxes.map((b) => `${b.dataset.b05Group}:${b.querySelector("legend")?.textContent}`),
|
||||
fields: box
|
||||
? [...box.querySelectorAll("[data-option-keys]")].map((n) => n.dataset.optionKeys)
|
||||
: [],
|
||||
loose: [...section().querySelectorAll("[data-option-keys]")].filter(
|
||||
(n) => !n.closest("[data-b05-group]") && visible(n),
|
||||
).length,
|
||||
links: box ? box.querySelectorAll(".b05-structure__summary").length : 0,
|
||||
twoRow: box ? box.querySelectorAll("[data-b05-summary=row]").length : 0,
|
||||
link: box?.querySelector(".b05-structure__summary-name")?.value.trim(),
|
||||
choices: form ? [...form.options].filter((o) => o.value).map((o) => o.textContent) : [],
|
||||
lamps: [...(listItem(KINDS[type][2])?.querySelectorAll(".b05-route__lamps > *") ?? [])].map(
|
||||
(d) => d.className,
|
||||
),
|
||||
};
|
||||
};
|
||||
const ok = (type, s) =>
|
||||
s.boxes.length === 1 &&
|
||||
s.boxes[0] === `${KINDS[type][0]}:${KINDS[type][1]}` &&
|
||||
JSON.stringify(s.fields) === JSON.stringify(TABLE[type]) &&
|
||||
s.loose === 0 &&
|
||||
s.links === 1 &&
|
||||
s.twoRow === 1 &&
|
||||
s.choices.length === KINDS[type][3] &&
|
||||
s.lamps.length === 1;
|
||||
const save = async () => {
|
||||
document.querySelector('[data-dock-action="save"]').click();
|
||||
await w(500);
|
||||
return until(() => !document.querySelector(".ui-overlay.is-active"), 60000);
|
||||
};
|
||||
const soilFile = async () =>
|
||||
(await api.fetchStructures(projectId)).structures.find((s) => s.type_id === "soil_guard");
|
||||
const out = { mode: MODE, page: location.hash };
|
||||
|
||||
if (MODE === "seed") {
|
||||
const now = await api.fetchStructures(projectId);
|
||||
sessionStorage.setItem("__5426_original", JSON.stringify(now.structures));
|
||||
const add = (type_id, at, options, extra = {}) => ({
|
||||
structure_id: null,
|
||||
type_id,
|
||||
placement: "interval",
|
||||
chainage_m: at,
|
||||
start_m: at,
|
||||
end_m: at + 10,
|
||||
options: { length_m: 10, before_m: 0, after_m: 10, ...options },
|
||||
memo: "54-26",
|
||||
...extra,
|
||||
});
|
||||
const next = [
|
||||
...now.structures,
|
||||
add(
|
||||
"soil_guard",
|
||||
400,
|
||||
{ form: "돌쌓기(찰)", height_m: 1.5 },
|
||||
{ group_items: { gsoilgdetail: LINK } },
|
||||
),
|
||||
add("berm", 560, { width_m: 1, height_m: 5 }),
|
||||
];
|
||||
await api.saveStructures(projectId, now.revision, next);
|
||||
for (const k of Object.keys(sessionStorage))
|
||||
if (k.includes("draft:structures") || k.includes("result:section-detail"))
|
||||
sessionStorage.removeItem(k);
|
||||
out.seeded = (await api.fetchStructures(projectId)).structures
|
||||
.filter((s) => s.type_id in KINDS)
|
||||
.map((s) => s.type_id);
|
||||
}
|
||||
|
||||
if (MODE === "check") {
|
||||
await until(() => document.querySelectorAll(".b05-route__irregular-list li").length > 0);
|
||||
const r = {};
|
||||
for (const [type, [, , re]] of Object.entries(KINDS)) {
|
||||
await open(re);
|
||||
r[type] = shape(type);
|
||||
r[type].ok = ok(type, r[type]);
|
||||
}
|
||||
await reset();
|
||||
out.allOk = Object.values(r).every((s) => s.ok);
|
||||
out.read = r;
|
||||
if (!location.hash.includes("b06")) sessionStorage.setItem("__5426_b05", JSON.stringify(r));
|
||||
else out.sameAsB05 = sessionStorage.getItem("__5426_b05") === JSON.stringify(r);
|
||||
}
|
||||
|
||||
if (MODE === "split") {
|
||||
await open(KINDS.soil_guard[2]);
|
||||
await set("length_m", 10);
|
||||
await set("before_m", 3);
|
||||
out.after = field("after_m").value;
|
||||
out.ok = Number(out.after) === 7;
|
||||
}
|
||||
|
||||
if (MODE === "height") {
|
||||
// 독립 벽 조정창은 위치 전용(8-29 지시) — 높이 = 좌측 칸 하나 · 횡단 그림이 그 값을 따라감
|
||||
await open(KINDS.soil_guard[2]);
|
||||
await w(1500);
|
||||
const card = () =>
|
||||
[...document.querySelectorAll(".b06-cross-card")].find((c) =>
|
||||
/^\s*20\+0400\.0m/.test(c.textContent),
|
||||
);
|
||||
const drawn = () => {
|
||||
const tip = card()?.querySelector(".b06-chart__culvert-revet title")?.textContent ?? "";
|
||||
return Number(tip.match(/H=([\d.]+)m/)?.[1]);
|
||||
};
|
||||
const near = (a, b) => Math.abs(a - b) < 0.01;
|
||||
out.steps = [];
|
||||
for (const h of [1.8, 2.3]) {
|
||||
await set("height_m", h);
|
||||
await until(() => near(drawn(), h), 8000);
|
||||
out.steps.push({ left: Number(field("height_m").value), drawn: drawn() });
|
||||
}
|
||||
out.ok = out.steps.every((s) => near(s.left, s.drawn));
|
||||
}
|
||||
|
||||
if (MODE === "save") {
|
||||
out.saved = await save();
|
||||
const soil = await soilFile();
|
||||
out.file = soil.options;
|
||||
out.ok =
|
||||
soil.options.form === "돌쌓기(찰)" &&
|
||||
soil.options.length_m === 10 &&
|
||||
soil.options.before_m === 3 &&
|
||||
soil.options.after_m === 7 &&
|
||||
soil.options.height_m === 2.3 &&
|
||||
JSON.stringify(soil.group_items) === JSON.stringify({ gsoilgdetail: LINK });
|
||||
}
|
||||
|
||||
if (MODE === "verify") {
|
||||
await open(KINDS.soil_guard[2]);
|
||||
const soil = await soilFile();
|
||||
out.form = TABLE.soil_guard.map((k) => field(k).value);
|
||||
out.file = TABLE.soil_guard.map((k) => String(soil.options[k]));
|
||||
out.ok = out.form.every((v, i) => Number(v) === Number(out.file[i]) || v === out.file[i]);
|
||||
await reset();
|
||||
}
|
||||
|
||||
if (MODE === "restore") {
|
||||
const original = JSON.parse(sessionStorage.getItem("__5426_original") || "null");
|
||||
if (!original) return JSON.stringify({ error: "원래 값 없음" });
|
||||
const now = await api.fetchStructures(projectId);
|
||||
await api.saveStructures(projectId, now.revision, original);
|
||||
for (const k of Object.keys(sessionStorage))
|
||||
if (
|
||||
k.includes("draft:structures") ||
|
||||
k.includes("result:section-detail") ||
|
||||
k.startsWith("__5426_")
|
||||
)
|
||||
sessionStorage.removeItem(k);
|
||||
const after = await api.fetchStructures(projectId);
|
||||
const key = (list) =>
|
||||
JSON.stringify(
|
||||
list.map((s) => [
|
||||
s.type_id,
|
||||
s.start_m,
|
||||
s.chainage_m,
|
||||
s.end_m,
|
||||
s.options,
|
||||
s.group_items ?? null,
|
||||
]),
|
||||
);
|
||||
out.restored = key(after.structures) === key(original);
|
||||
}
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user