Merge remote-tracking branches 'origin/main_laptop_1' and 'origin/sub_laptop_8' into sub_desktop_1
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"판": 6,
|
||||
"판": 7,
|
||||
"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`가 새 키로 옮긴다.",
|
||||
"구조물군": {
|
||||
@@ -676,30 +676,7 @@
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"key": "ditch_spec",
|
||||
"label": "규격",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"콘크리트 개거 150×200",
|
||||
"L형수로 H=0.2"
|
||||
],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 실무 붙박이(콘크리트 개거)로 돎"
|
||||
},
|
||||
{
|
||||
"key": "length_m",
|
||||
"label": "연장",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"empty_means": "비우면 표가 안 서고 「연장을 적어야 섬」 사유가 뜸 — 개거는 m당 원단위라 연장이 곧 밑수(2026-09-14 A2)"
|
||||
}
|
||||
]
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"type_id": "ditch_side",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -366,6 +366,7 @@ def test_a6_a7_types_exist_for_manual_placement():
|
||||
item = by_id[type_id]
|
||||
assert item.group == "A" and item.managed_by is None
|
||||
assert item.placement == "point"
|
||||
assert not item.options, f"{type_id} 옵션 없음 — 기준 측점 + 종류 단위 잇기만(PLAN 52-5)"
|
||||
|
||||
|
||||
def test_ford_bridge_shares_pipe_kind_and_diameter():
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/* 52-5 — B05 · B06 좌측 「구조물 배치」 노출형 횡단수로 · 개거(겉도랑) = 사용자 구조물 표 05
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(랩탑_보조) · 폼 비어 있음 · 로그인 상태
|
||||
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m525 = "단계";` 를 붙여
|
||||
* orca eval --expression "window.__m525='edit'; $(cat 이 파일)" → 두 종류 모양 · 추가 · 잇기 · [저장]
|
||||
* (orca reload 로 다시 열기)
|
||||
* orca eval --expression "window.__m525='verify'; $(cat 이 파일)" → 저장 값 그대로 · 다시 고름
|
||||
* (B06 으로 옮겨 — 같은 모양)
|
||||
* orca eval --expression "window.__m525='verify'; $(cat 이 파일)" → B06 에서도 같은 값 · 같은 모양
|
||||
* orca eval --expression "window.__m525='restore'; $(cat 이 파일)" → 추가한 둘 삭제 · [저장]
|
||||
* 쓰는 것: 검증 프로젝트 structures.json(되돌림 단계가 지움)
|
||||
* 기대 수치:
|
||||
* shape(종류마다) — optionInputs 0 · containers 0 · summaryRows 1 · summaryVisible true · summaryRightAfterPosition true ·
|
||||
* anchor true · stale false
|
||||
* edit — added 둘 · 잇기 이름 값 있음 · 저장 뒤 목록 2 늘어남
|
||||
* verify — 같은 모양 · 잇기 이름 그대로 · 옵션 값 없음
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m525 || "edit";
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const until = async (fn, ms = 15000) => {
|
||||
for (let t = 0; t < ms; t += 200) {
|
||||
if (fn()) return true;
|
||||
await w(200);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const vis = (n) => !!n && n.offsetParent !== null;
|
||||
const sec = () => document.querySelector(".b05-structure-section");
|
||||
const body = () => sec().querySelector(".b05-route__panel-body");
|
||||
const selects = () => sec().querySelectorAll(".b05-structure__grid select");
|
||||
const fire = (el, value) => {
|
||||
el.value = value;
|
||||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
};
|
||||
const listItems = () => [...document.querySelectorAll(".b05-route__irregular-list li")];
|
||||
const NAMES = { cross_drain_exposed: "노출형 횡단수로", open_ditch: "개거(겉도랑)" };
|
||||
const AT = { cross_drain_exposed: [2, 7], open_ditch: [6, 3] };
|
||||
|
||||
const shape = (typeId) => {
|
||||
const kids = [...body().children];
|
||||
const position = kids.find((c) => c.classList.contains("b05-structure__position-row"));
|
||||
const summary = kids.find((c) => c.classList.contains("b05-structure__summary"));
|
||||
const optionRow = kids.filter((c) => c.classList.contains("b05-structure__grid"))[1];
|
||||
const after = kids.slice(kids.indexOf(position) + 1).find((c) => vis(c));
|
||||
const anchorWrap = position.querySelectorAll(":scope > label")[2];
|
||||
return {
|
||||
typeId,
|
||||
optionInputs: vis(optionRow) ? optionRow.querySelectorAll("input, select").length : 0,
|
||||
containers: [...sec().querySelectorAll("[data-spec-container]")].filter(vis).length,
|
||||
summaryRows: [...sec().querySelectorAll("[data-b05-summary=row]")].filter(vis).length,
|
||||
summaryVisible: vis(summary),
|
||||
summaryRightAfterPosition: after === summary,
|
||||
anchor: vis(anchorWrap),
|
||||
stale: vis(sec().querySelector(".b05-structure__stale")),
|
||||
linked: summary.querySelector(".b05-structure__summary-name")?.textContent ?? "",
|
||||
};
|
||||
};
|
||||
const pickType = async (typeId) => {
|
||||
const [group, type] = selects();
|
||||
fire(group, "A");
|
||||
await w(300);
|
||||
fire(type, typeId);
|
||||
await w(500);
|
||||
};
|
||||
const linkFirstLeaf = async () => {
|
||||
sec().querySelector("[data-b05-summary=pick]").click();
|
||||
await until(() => document.querySelector(".ui-item-picker"));
|
||||
await w(600);
|
||||
const modal = document.querySelector(".ui-item-picker");
|
||||
// 끝 항목 = ▸ · ▾ 로 안 시작하고 모달 조작 단추가 아닌 첫 단추
|
||||
const ctl = ["닫기", "개인용으로 복사", "관리 화면에서 고치기", "해제", "적용"];
|
||||
const leaf = [...modal.querySelectorAll("button")].find((b) => {
|
||||
const t = b.textContent.trim();
|
||||
return t && !/^[▸▾]/.test(t) && !ctl.includes(t);
|
||||
});
|
||||
leaf?.click();
|
||||
await w(300);
|
||||
[...modal.querySelectorAll("button")].find((b) => b.textContent.trim() === "적용")?.click();
|
||||
await w(500);
|
||||
return !!leaf;
|
||||
};
|
||||
const clickBtn = async (text) => {
|
||||
[...document.querySelectorAll("button")]
|
||||
.find((b) => b.textContent.trim() === text && vis(b))
|
||||
?.click();
|
||||
await w(1500);
|
||||
};
|
||||
const out = { mode: MODE, items: [] };
|
||||
|
||||
if (MODE === "edit") {
|
||||
await until(() => listItems().length > 0);
|
||||
out.before = listItems().length;
|
||||
for (const typeId of Object.keys(NAMES)) {
|
||||
await pickType(typeId);
|
||||
const row = { ...shape(typeId), phase: "edit" };
|
||||
const [no, dist] = sec()
|
||||
.querySelectorAll(".b05-structure__position-row > label")[2]
|
||||
.querySelectorAll("input");
|
||||
fire(no, String(AT[typeId][0]));
|
||||
fire(dist, String(AT[typeId][1]));
|
||||
await w(200);
|
||||
row.picked = await linkFirstLeaf();
|
||||
row.linked = shape(typeId).linked;
|
||||
sec().querySelector(".is-primary").click(); // 추가
|
||||
await w(800);
|
||||
out.items.push(row);
|
||||
}
|
||||
out.after = listItems().length;
|
||||
await clickBtn("저장");
|
||||
out.listed = listItems()
|
||||
.map((l) => l.textContent.trim())
|
||||
.filter((t) => /횡단수로|개거/.test(t));
|
||||
} else if (MODE === "verify") {
|
||||
await until(() => listItems().length > 0);
|
||||
for (const typeId of Object.keys(NAMES)) {
|
||||
const li = listItems().find((l) => l.textContent.includes(NAMES[typeId]));
|
||||
if (!li) {
|
||||
out.items.push({ typeId, missing: true });
|
||||
continue;
|
||||
}
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(800);
|
||||
// B06 은 다른 항목이 골라져 있으면 첫 눌림이 풀기만 함 — 안 불러와졌으면 한 번 더
|
||||
if (selects()[1].value !== typeId) {
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(800);
|
||||
}
|
||||
out.items.push({ ...shape(typeId), phase: "verify", title: li.title || "" });
|
||||
(li.querySelector("button") ?? li).click(); // 재선택 = 해제
|
||||
await w(300);
|
||||
}
|
||||
} else if (MODE === "restore") {
|
||||
await until(() => listItems().length > 0);
|
||||
for (const typeId of Object.keys(NAMES)) {
|
||||
const li = listItems().find((l) => l.textContent.includes(NAMES[typeId]));
|
||||
if (!li) continue;
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(600);
|
||||
await clickBtn("삭제");
|
||||
}
|
||||
await clickBtn("저장");
|
||||
out.left = listItems().length;
|
||||
}
|
||||
const rows = out.items.filter((r) => !r.missing);
|
||||
out.shapeOk =
|
||||
rows.length === 2 &&
|
||||
rows.every(
|
||||
(r) =>
|
||||
r.optionInputs === 0 &&
|
||||
r.containers === 0 &&
|
||||
r.summaryRows === 1 &&
|
||||
r.summaryVisible &&
|
||||
r.summaryRightAfterPosition &&
|
||||
r.anchor &&
|
||||
!r.stale,
|
||||
);
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user