merge: main_desktop_1 52-9 — 충돌(구조물정의 판 · 그룹) = 양쪽 그룹 다 살림 · 판 9 · 판 8 이력 · Panel_Options = 메인 세 조건
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FJqWQpoB1RE7hiaQ2wH8qV
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"판": 8,
|
||||
"판": 9,
|
||||
"schema_version": 1,
|
||||
"comment": "B05 구조물 타입 레지스트리 정본. 근거·옵션 상세는 PLAN.md 구조물 리스트와 resources/knowledge/technical_info/01_임도 참조. 2026-08-17 선택지 재편: B군(종단배수)·F군(생태/녹화)·G군(노면공)은 enabled:false 보존 — B05 선택지에서 빠지고 B06 개별 횡단도 옵션으로 재사용한다. 바닥막이(bed_sill)·생태연못(eco_pond)은 임도 미사용 확정으로 삭제. 배관·BOX암거·물넘이·세월교는 pipe_points.json 정본 관리(managed_by) — structures.json에 저장하지 않으며, 배관 부속(유형/집수정/기슭막이/돌붙임)·날개벽 옵션 정의는 B06/B07 필수 승격의 선언 근거다. ★기본값 원칙(2026-08-16 크로스체크 2차): default를 두는 것은 ①법정이 등급과 무관하게 못박은 단일 수치 ②사용자가 개발 단계에서 확정한 값 ③도메인 수치가 아닌 표시용 문자열, 이 셋뿐이다. 재료·형식 같은 설계자 판단 선택지와 미확정 제원은 default 없이 required로 두어 사용자가 직접 고르게 한다 (pipe_diameter_mm 1000 = 별표2 원칙값+사용자 확정 — 화이트리스트 등재). BOX암거 본체 2.0×2.0·날개벽 유입/유출 있음·짧은쪽 높이 1m·길이 2m·각도 45°는 2026-08-17 사용자 확정값이다 — 본체 규격은 실무 관측 규격(2.0×2.0·3.0×3.0)을 폼에서 프리셋 select로 받고 필요 시 사용자 지정으로 자유 입력하되, 저장 키는 body_width_m·body_height_m 숫자 그대로다(프리셋 전용 키를 만들지 않는다). 이 값들은 B05 서브폼이 실제로 받으므로 phase를 detail로 두지 않는다. ★보호공 개편(2026-08-17 사용자 지시): 구 `*_pitching`(있음/없음) + `*_pitching_finish`(찰/메) 두 축을 `*_protection` 한 축(돌붙임(찰)·돌붙임(메)·도수로)으로 합쳤다 — 도수로·산비탈수로(B4)는 유출부에서 돌붙임 대신 쓰는 공종이라 별도 구조물이 아니라 보호공 선택지로 받는다. \"없음\"은 삭제(물이 흐르는 자리라 보호공 필수). 치수는 배타 — 돌붙임이면 `*_protection_area_m2`(10㎡), 도수로면 `*_protection_width_m`(1m, 지식DB 수치 근거 없음·사용자 확정, 실무 확인 후 개선). 구 저장분은 `common_util_drainage_pipes.parse_pipe_points`가 새 키로 옮긴다.",
|
||||
"구조물군": {
|
||||
@@ -2838,7 +2838,7 @@
|
||||
{
|
||||
"type_id": "spoil_bank",
|
||||
"group": "E",
|
||||
"name": "유용토운반작업장(구 사토장)",
|
||||
"name": "유용토운반작업장(사토장)",
|
||||
"placement": "interval",
|
||||
"style": {
|
||||
"color": "#8c9a2e",
|
||||
@@ -2850,56 +2850,15 @@
|
||||
"quantity"
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"key": "side",
|
||||
"label": "쌓는 쪽",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"자동(성토 쪽)",
|
||||
"좌",
|
||||
"우"
|
||||
],
|
||||
"default": "자동(성토 쪽)",
|
||||
"required": false,
|
||||
"phase": "b05",
|
||||
"default_basis": "「자동(성토 쪽)」 — 흙은 성토 쪽에 쌓는다는 지금까지의 동작을 이어받는 표식. 측점마다 좌·우로 바꿈"
|
||||
},
|
||||
{
|
||||
"key": "fill_slope_ratio",
|
||||
"label": "비탈 기울기 (1:n 의 n)",
|
||||
"input": "number",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 그 측점의 노선 성토 기울기를 그대로 씀 — 지식DB 가 「기본 비탈기울기는 근거에 없다」고 못 박은 자리"
|
||||
},
|
||||
{
|
||||
"key": "extra_distance_m",
|
||||
"label": "추가 운반거리",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 0,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"default_basis": "0 = 「추가 없음」 — 노선 안 사토장은 측점 누가거리로 거리가 나오므로 이 칸은 외부 사토장 대비 여벌"
|
||||
},
|
||||
{
|
||||
"key": "area_m2",
|
||||
"label": "면적",
|
||||
"input": "number",
|
||||
"unit": "㎡",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "capacity_m3",
|
||||
"label": "용량",
|
||||
"label": "사토장 부피",
|
||||
"input": "number",
|
||||
"unit": "㎥",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
"required": false,
|
||||
"phase": "b05",
|
||||
"empty_means": "비우면 B06 사토 배분이 이 사토장을 건너뜀"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2917,23 +2876,15 @@
|
||||
"quantity"
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"key": "area_m2",
|
||||
"label": "면적",
|
||||
"input": "number",
|
||||
"unit": "㎡",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "capacity_m3",
|
||||
"label": "용량",
|
||||
"label": "토취장 부피",
|
||||
"input": "number",
|
||||
"unit": "㎥",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
"required": false,
|
||||
"phase": "b05",
|
||||
"empty_means": "비우면 자리만 잡음 — 부피는 설계자가 현장 조사로 손 입력"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -3394,6 +3345,26 @@
|
||||
],
|
||||
"형상": {}
|
||||
}
|
||||
],
|
||||
"spoil_bank": [
|
||||
{
|
||||
"id": "gspoildetail",
|
||||
"이름": "상세 내용",
|
||||
"옵션": [
|
||||
"capacity_m3"
|
||||
],
|
||||
"형상": {}
|
||||
}
|
||||
],
|
||||
"borrow_pit": [
|
||||
{
|
||||
"id": "gborrowdetail",
|
||||
"이름": "상세 내용",
|
||||
"옵션": [
|
||||
"capacity_m3"
|
||||
],
|
||||
"형상": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"배치": {},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,74 @@
|
||||
"""PLAN 52-9 — 안전부대용지 › 사토장 · 토취장 = 사용자 구조물 표 08.
|
||||
|
||||
· 있는 종류 키 그대로(spoil_bank · borrow_pit) · 기준 측점 + 컨테이너 「상세 내용」(정의 그룹) 하나
|
||||
· 부피 손 입력 = 옛 용량 키 `capacity_m3`(B06 사토 배분이 읽는 자리)
|
||||
· 옛 칸은 정의에서 빼 지난 정의 값
|
||||
· 잇기 = 그룹 id(`group_items`) — 집계 표가 그룹마다 한 줄
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from B05_Profile.B05_Profile_Structures_Schema import DEFINITION_PATH, load_structure_types
|
||||
from B06_Section.B06_Section_Engine_SpoilFill import spoil_sites
|
||||
from M02_MasterTemplete.M02_Table_Fill import _with_group_items
|
||||
from M02_MasterTemplete.StructOptions import M02_SO_Definition
|
||||
from M02_MasterTemplete.StructOptions.M02_SO_Groups import groups_of
|
||||
|
||||
KINDS = {
|
||||
"spoil_bank": ("유용토운반작업장(사토장)", "사토장 부피", "gspoildetail"),
|
||||
"borrow_pit": ("토사채취장(토취장)", "토취장 부피", "gborrowdetail"),
|
||||
}
|
||||
|
||||
|
||||
def test_옵션은_부피_하나_손_입력():
|
||||
by_id = {t.type_id: t for t in load_structure_types()}
|
||||
for type_id, (name, label, _) in KINDS.items():
|
||||
item = by_id[type_id]
|
||||
assert item.group == "E" and item.name == name and item.managed_by is None
|
||||
assert [o.key for o in item.options] == ["capacity_m3"]
|
||||
volume = item.options[0]
|
||||
assert volume.label == label and volume.input == "number" and volume.unit == "㎥"
|
||||
assert volume.phase == "b05" and not volume.required and volume.default is None
|
||||
|
||||
|
||||
def test_컨테이너_상세_내용_하나_부피를_담음():
|
||||
groups = groups_of()
|
||||
for type_id, (_, _, gid) in KINDS.items():
|
||||
assert groups[type_id] == [{"id": gid, "name": "상세 내용", "options": ["capacity_m3"]}]
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
def test_잇기는_그룹마다_집계_한_줄():
|
||||
link = {"source": "SS", "code": "SS-000001", "name": "사토장"}
|
||||
rows = _with_group_items(
|
||||
[
|
||||
{
|
||||
"type_id": "spoil_bank",
|
||||
"복사본": "s1",
|
||||
"summary_item": None,
|
||||
"group_items": {"gspoildetail": link},
|
||||
}
|
||||
]
|
||||
)
|
||||
assert [r["복사본"] for r in rows] == ["s1", "s1__gspoildetail"]
|
||||
assert rows[1]["summary_item"] == link and rows[1]["그룹"] == "상세 내용"
|
||||
|
||||
|
||||
def test_B06_사토_배분은_부피를_그대로_읽음_옛_값도():
|
||||
new = {"type_id": "spoil_bank", "start_m": 20, "end_m": 40, "options": {"capacity_m3": 150}}
|
||||
old = {
|
||||
"type_id": "spoil_bank",
|
||||
"start_m": 60,
|
||||
"end_m": 80,
|
||||
"options": {"capacity_m3": 90, "side": "좌", "area_m2": 30, "extra_distance_m": 5},
|
||||
}
|
||||
sites = spoil_sites([new, old])
|
||||
assert [s["capacity_m3"] for s in sites] == [150.0, 90.0]
|
||||
assert sites[0]["side_option"] is None and sites[1]["side_option"] == "좌"
|
||||
assert sites[1]["extra_distance_m"] == 5
|
||||
@@ -0,0 +1,215 @@
|
||||
/* 52-9 — B05 · B06 좌측 「구조물 배치」 사토장 · 토취장 = 사용자 구조물 표 08
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(데스크탑_메인)
|
||||
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m529 = "단계";` 를 붙여
|
||||
* seed → 원래 구조물 목록을 sessionStorage 에 두고 옛 꼴 사토장 하나를 심음(PUT) · 그 뒤 orca reload
|
||||
* edit → 옛 사토장 = 지난 정의 값 · 새 사토장 · 토취장 추가(부피 · 잇기) · [저장] · 그 뒤 orca reload
|
||||
* verify → 다시 연 값 — B05 에서 한 번 · B06(#/b06-section) 에서 한 번(B05 = B06 그리기)
|
||||
* restore → 원래 구조물 목록으로 되돌림(PUT) · 초안 비움
|
||||
* 쓰는 것: 검증 프로젝트 structures.json(되돌림 단계가 원래 값으로 다시 씀)
|
||||
* 기대 수치(edit): 컨테이너 1(상세 내용) · 칸 = 사토장 부피 / 토취장 부피 · 종류 단위 잇기 줄 가림 ·
|
||||
* 옛 사토장 지난 정의 값 = side · fill_slope_ratio · extra_distance_m · area_m2 · 부피 90
|
||||
* 기대 수치(verify): 부피 150 · 80 · 잇기 지정 · 옛 키 그대로 · B05 = B06
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m529 || "edit";
|
||||
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 GID = { spoil_bank: "gspoildetail", borrow_pit: "gborrowdetail" };
|
||||
const section = () => document.querySelector(".b05-structure-section");
|
||||
const selects = () => section().querySelectorAll(".b05-structure__grid select");
|
||||
const change = async (el, v) => {
|
||||
el.value = v;
|
||||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await w(250);
|
||||
};
|
||||
const station = async (index, no, plus) => {
|
||||
const wrap = section().querySelector(".b05-structure__position-row").children[index];
|
||||
const [s, r] = wrap.querySelectorAll("input");
|
||||
s.value = String(no);
|
||||
s.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
r.value = String(plus);
|
||||
r.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await w(250);
|
||||
};
|
||||
const box = (type) => section().querySelector(`[data-b05-group="${GID[type]}"]`);
|
||||
const volume = (type) => box(type)?.querySelector('[data-option-keys="capacity_m3"] input');
|
||||
const linked = (type) =>
|
||||
!!box(type)?.querySelector(".b05-structure__summary-name")?.classList.contains("is-assigned");
|
||||
const shape = (type) => ({
|
||||
boxes: [...section().querySelectorAll("[data-b05-group]")].map((b) => ({
|
||||
id: b.dataset.b05Group,
|
||||
legend: b.querySelector("legend").textContent,
|
||||
fields: [...b.querySelectorAll("[data-option-keys]")].map(
|
||||
(n) => `${n.dataset.optionKeys}=${n.querySelector("span")?.textContent}`,
|
||||
),
|
||||
linked: linked(type),
|
||||
})),
|
||||
typeRowHidden: section().querySelector(".b05-structure__summary").hidden,
|
||||
// 컨테이너 밖에 보이는 옵션 칸(0 이어야 함 — 부피는 컨테이너 안)
|
||||
looseFields: [...section().querySelectorAll("[data-option-keys]")].filter(
|
||||
(n) => !n.closest("[data-b05-group]") && n.offsetParent !== null,
|
||||
).length,
|
||||
stale: [...section().querySelectorAll(".b05-structure__stale input")].map((i) =>
|
||||
[i.closest(".b05-route__field")?.querySelector("span")?.textContent, i.value].join("="),
|
||||
),
|
||||
volume: volume(type)?.value,
|
||||
});
|
||||
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 pickLink = async (type) => {
|
||||
box(type).querySelector('[data-b05-summary="pick"]').click();
|
||||
await until(() => document.querySelector(".ui-item-picker__tree .ui-tree__row"));
|
||||
for (let k = 0; k < 8; k++) {
|
||||
const rows = [...document.querySelectorAll(".ui-item-picker__tree .ui-tree__row")];
|
||||
const leaf = rows.find((r) => !r.classList.contains("is-branch"));
|
||||
if (leaf) {
|
||||
leaf.click();
|
||||
break;
|
||||
}
|
||||
rows.find((r) => r.querySelector(".ui-tree__caret")?.textContent === "▸")?.click();
|
||||
await w(300);
|
||||
}
|
||||
await w(300);
|
||||
[...document.querySelectorAll(".ui-item-picker__actions button")]
|
||||
.find((b) => b.textContent.includes("적용"))
|
||||
.click();
|
||||
await w(600);
|
||||
};
|
||||
const save = async () => {
|
||||
document.querySelector('[data-dock-action="save"]').click();
|
||||
await w(500);
|
||||
return until(() => !document.querySelector(".ui-overlay.is-active"), 60000);
|
||||
};
|
||||
const mine = (list) =>
|
||||
list
|
||||
.filter((s) => s.type_id in GID)
|
||||
.map((s) => ({
|
||||
type: s.type_id,
|
||||
at: [s.start_m, s.chainage_m, s.end_m],
|
||||
options: s.options,
|
||||
group_items: Object.keys(s.group_items ?? {}),
|
||||
}));
|
||||
const out = { mode: MODE };
|
||||
|
||||
if (MODE === "seed") {
|
||||
const now = await api.fetchStructures(projectId);
|
||||
sessionStorage.setItem("__529_original", JSON.stringify(now.structures));
|
||||
const old = {
|
||||
structure_id: null,
|
||||
type_id: "spoil_bank",
|
||||
placement: "interval",
|
||||
chainage_m: 370,
|
||||
start_m: 360,
|
||||
end_m: 380,
|
||||
options: {
|
||||
side: "좌",
|
||||
fill_slope_ratio: 1.5,
|
||||
extra_distance_m: 5,
|
||||
area_m2: 30,
|
||||
capacity_m3: 90,
|
||||
},
|
||||
memo: "52-9 옛 꼴",
|
||||
};
|
||||
const saved = await api.saveStructures(projectId, now.revision, [...now.structures, old]);
|
||||
for (const k of Object.keys(sessionStorage))
|
||||
if (k.includes("draft:structures")) sessionStorage.removeItem(k);
|
||||
out.seeded = mine(saved.structures ?? (await api.fetchStructures(projectId)).structures);
|
||||
}
|
||||
|
||||
if (MODE === "edit") {
|
||||
// ① 옛 꼴 사토장 — 컨테이너 하나 · 옛 칸은 지난 정의 값 · 부피 = 옛 용량 값
|
||||
await open(/^18\+0~19\+0.*사토장/);
|
||||
out.old = shape("spoil_bank");
|
||||
// ② 새 사토장 25+0 ~ 26+0(기준 25+10) · 부피 150
|
||||
section().querySelector(".b05-route__irregular-actions button:last-child").click(); // 리셋
|
||||
await w(400);
|
||||
await change(selects()[0], "E");
|
||||
await change(selects()[1], "spoil_bank");
|
||||
await station(1, 25, 0);
|
||||
await station(3, 26, 0);
|
||||
await station(2, 25, 10);
|
||||
out.newSpoilForm = shape("spoil_bank");
|
||||
await change(volume("spoil_bank"), "150");
|
||||
section().querySelector(".b05-route__irregular-actions .is-primary").click();
|
||||
await w(800);
|
||||
// ③ 새 토취장 28+0 · 부피 80
|
||||
section().querySelector(".b05-route__irregular-actions button:last-child").click();
|
||||
await w(400);
|
||||
await change(selects()[0], "E");
|
||||
await change(selects()[1], "borrow_pit");
|
||||
await station(2, 28, 0);
|
||||
out.newBorrowForm = shape("borrow_pit");
|
||||
await change(volume("borrow_pit"), "80");
|
||||
section().querySelector(".b05-route__irregular-actions .is-primary").click();
|
||||
await w(800);
|
||||
// ④ 잇기 — 새 사토장 · 토취장 컨테이너마다
|
||||
await open(/^25\+0~26\+0.*사토장/);
|
||||
await pickLink("spoil_bank");
|
||||
out.linkSpoil = linked("spoil_bank");
|
||||
await open(/^28\+0.*토취장/);
|
||||
await pickLink("borrow_pit");
|
||||
out.linkBorrow = linked("borrow_pit");
|
||||
out.saved = await save();
|
||||
out.file = mine((await api.fetchStructures(projectId)).structures);
|
||||
}
|
||||
|
||||
// 다시 연 값 — B05 에서 한 번(값을 sessionStorage 에 둠) · B06(#/b06-section)에서 한 번(같은지 견줌)
|
||||
if (MODE === "verify") {
|
||||
await until(() => document.querySelectorAll(".b05-route__irregular-list li").length > 0);
|
||||
const r = {};
|
||||
await open(/^18\+0~19\+0.*사토장/);
|
||||
r.old = shape("spoil_bank");
|
||||
await open(/^25\+0~26\+0.*사토장/);
|
||||
r.spoil = shape("spoil_bank");
|
||||
await open(/^28\+0.*토취장/);
|
||||
r.borrow = shape("borrow_pit");
|
||||
section().querySelector(".b05-route__irregular-actions button:last-child").click(); // 리셋
|
||||
out.page = location.hash;
|
||||
out.read = r;
|
||||
if (location.hash.includes("b05")) sessionStorage.setItem("__529_b05", JSON.stringify(r));
|
||||
else out.sameDrawing = sessionStorage.getItem("__529_b05") === JSON.stringify(r);
|
||||
}
|
||||
|
||||
if (MODE === "restore") {
|
||||
const original = JSON.parse(sessionStorage.getItem("__529_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.startsWith("__529_")) 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]),
|
||||
);
|
||||
out.restored = key(after.structures) === key(original);
|
||||
out.left = mine(after.structures).length;
|
||||
}
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user