Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_4

This commit is contained in:
2026-10-05 23:37:17 +09:00
10 changed files with 3581 additions and 28 deletions
+2 -1
View File
@@ -327,7 +327,8 @@ def _wing_spec(values: dict[str, Any], defaults: dict[str, Any], side: str) -> d
height = _number(
values.get(f"{prefix}_height_m"), _number(defaults.get(f"{prefix}_height_m"), 0.0)
)
installed = str(install or "").strip() != "없음"
# BOX암거 유입·유출 구조(표 02) — 없음 · 날개벽 · 흙막이 · 옛 값 있음 = 날개벽. 날개벽만 세움.
installed = str(install or "").strip() not in ("없음", "흙막이")
extend = (length or 0.0) * math.cos(math.radians(angle or 0.0)) if installed else 0.0
return {
"installed": installed,
+2 -1
View File
@@ -241,7 +241,8 @@ function wingSpec(
const length = num(values[`${prefix}_length_m`], num(defaults[`${prefix}_length_m`], 0.0));
const angle = num(values[`${prefix}_angle_deg`], num(defaults[`${prefix}_angle_deg`], 45.0));
const height = num(values[`${prefix}_height_m`], num(defaults[`${prefix}_height_m`], 0.0));
const installed = String(install ?? "").trim() !== "없음";
// BOX암거 유입·유출 구조(표 02) — 없음 · 날개벽 · 흙막이 · 옛 값 있음 = 날개벽. 날개벽만 세움.
const installed = !["없음", "흙막이"].includes(String(install ?? "").trim());
return {
installed,
height_m: height,
@@ -0,0 +1,25 @@
# 구조물 표 07 — 안전부대용지 · 종단배수 · 계류사방 (사용자 10-5 · 원문 그대로)
사용자: 다음 종단배수 전체 항목은 구조물 연결 지정 없으며 현재처럼 숨김처리. 계류사방 항목도 숨김처리
구조물군 상세 종류 기본 옵션 컨테이너 1 컨테이너 1 옵셩 항목
안전부대용지 대피로 측점 No, + 측점거리 (필수)컨테이너 제목 : 상세 내용
상세 대피소, 정차/작업장, 차돌림곳
컨테이너 없는 항목 측점 하단에 구조물 조합 연결 항목 넣기
안전부대용지 반사경 측점 No, + 측점거리
안전부대용지 경계석 측점 No, + 측점거리
안전부대용지 위험표시 측점 No, + 측점거리
안전부대용지 국가지점번호판 측점 No, + 측점거리
안전부대용지 취수정 측점 No, + 측점거리
안전부대용지 준공표지판 측점 No, + 측점거리
안전부대용지 차단기 측점 No, + 측점거리
안전부대용지 취수정 측점 No, + 측점거리 (원문에 두 번 · 하나로 봄)
(덧 · 사용자 10-5)
구조물군 상세 종류 기본 옵션 컨테이너 1 컨테이너 1 옵셩 항목
(필수)컨테이너 제목 : 상세 내용
안전부대용지 가드레일 측점 No, + 측점거리 형태 수기입력
길이 수기입력
기준측점 전 수기입력(길이 + 기준측점 전/후에 따라 연동 계산)
기준측점 후 수기입력(길이 + 기준측점 전/후에 따라 연동 계산)
@@ -0,0 +1,17 @@
# 구조물 표 08 — 안전부대용지 사토장 · 토취장 · 노면공 콘크리트 포장 · 기타 (사용자 10-5 · 원문 그대로)
구조물군 상세 종류 기본 옵션 컨테이너 1 컨테이너 1 옵셩 항목 … 상세 설계 필수 선택
안전부대용지 유용토운반작업장(사토장) 측점 No, + 측점거리 (필수)컨테이너 제목 : 상세 내용
사토장 부피 수기입력
안전부대용지 토사채취장(토취장) 측점 No, + 측점거리 (필수)컨테이너 제목 : 상세 내용
토취장 부피 수기입력
노면공 콘크리트 포장 측점 No, + 측점거리 (필수)컨테이너 제목 : 상세 내용
포장사양 콘크리트, XXX
높이 수기입력
기타 수기입력 측점 No, + 측점거리 (선택)컨테이너 제목 : 상세 내용 (상세 설계) 도수로(or 돌붙임)
비고 수기 입력 (상세 설계) 도수로(or 돌붙임) 관보호공, 옹벽, 돌쌓기, 흙막이, 큰톨쌓기 가 같은 형상으로 보임.
(가드레일은 표 07 덧과 같음)
+2 -25
View File
@@ -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():
+8
View File
@@ -45,6 +45,14 @@ def test_box_wing_extends_slab() -> None:
assert spec["wing_in"]["slab_extend_m"] == round(2 * math.cos(math.radians(45)), 3)
def test_box_structure_choices() -> None:
"""유입·유출 구조(표 02) — 날개벽 · 옛 값 있음만 날개벽을 세움 · 흙막이 · 없음은 안 세움."""
for value, installed in (("날개벽", True), ("있음", True), ("흙막이", False), ("없음", False)):
spec = _box_set({"wing_in": value, "wing_in_length_m": 2})
assert spec["wing_in"]["installed"] is installed
assert (spec["wing_in"]["slab_extend_m"] > 0) is installed
def test_attach_box_only_owner_station(tmp_path: Path) -> None:
"""BOX암거는 **소유 측점 한 곳에만** `box` 키로 붙는다(2026-08-25 사용자 확정).
@@ -58,7 +58,13 @@ POINTS = [
{
"chainage_m": 80.0,
"facility": "box_culvert",
"options": {"body_width_m": 3.0, "body_height_m": 2.5, "wing_out_length_m": 2.0},
"options": {
"body_width_m": 3.0,
"body_height_m": 2.5,
"wing_in": "날개벽",
"wing_out": "흙막이",
"wing_out_length_m": 2.0,
},
},
{
"chainage_m": 100.0,
@@ -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);
})();