Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_4
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 덧과 같음)
|
||||
@@ -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():
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
Reference in New Issue
Block a user