diff --git a/B05_Profile/B05_Profile_Api_Structures.ts b/B05_Profile/B05_Profile_Api_Structures.ts index d892eea8..f7f83289 100644 --- a/B05_Profile/B05_Profile_Api_Structures.ts +++ b/B05_Profile/B05_Profile_Api_Structures.ts @@ -48,6 +48,9 @@ export interface StructureType { drawing_views: string[]; /** 다른 정본이 관리하는 타입(배관 = pipe_points.json) — 구조물 목록에 넣지 않는다. */ managed_by: string | null; + /** 목록에는 두되 **제원·수량을 내는 주인이 다른 화면**인 타입 — 그 화면 이름. + * 측구(옆도랑) = `"횡단 설계"`. 항목에 「~에서 관리」를 붙이고 수량 집계는 건너뛴다. */ + design_owner: string | null; reference_only: boolean; enabled: boolean; } @@ -212,7 +215,6 @@ export function pipesToStructureMarks( })) as StructureInstance[]; } - export function structureAnchorM(structure: StructureInstance): number { return structure.chainage_m ?? structure.start_m ?? 0; } diff --git a/B05_Profile/B05_Profile_Structure_Types.json b/B05_Profile/B05_Profile_Structure_Types.json index 9a59af11..f6862a92 100644 --- a/B05_Profile/B05_Profile_Structure_Types.json +++ b/B05_Profile/B05_Profile_Structure_Types.json @@ -448,6 +448,7 @@ "enabled": false, "group": "B", "name": "측구(옆도랑)", + "design_owner": "횡단 설계", "placement": "interval", "style": { "color": "#2eaadc", diff --git a/B05_Profile/B05_Profile_Structures_Schema.py b/B05_Profile/B05_Profile_Structures_Schema.py index 1afe9e16..d570578e 100644 --- a/B05_Profile/B05_Profile_Structures_Schema.py +++ b/B05_Profile/B05_Profile_Structures_Schema.py @@ -65,6 +65,14 @@ class StructureType(BaseModel): drawing_views: list[str] = Field(default_factory=list) # 다른 정본이 관리하는 타입(배관 = pipe_points.json). structures.json에 저장하지 않는다. managed_by: str | None = None + # 목록에는 두되 **제원·수량을 내는 주인이 다른 화면**인 타입 — 그 화면 이름을 적는다 + # (2026-09-07 사용자: 「두되 표시만 해줘」). `managed_by`와 달리 저장은 그대로 되고, + # ① 화면이 「{이름}에서 관리」 표시를 붙이고 ② 수량 집계가 건너뛴다. + # + # 측구(옆도랑)가 그 경우다 — 횡단 설계가 측구 켬/끔·형식·터파기 단면적을 이미 셈하므로 + # (`B06_Section_Engine_Design.py` · `common_util_cross_design.ts` 짝), 구조물로 또 세면 + # **같은 것을 두 번 계상**한다(2026-09-07 조사). + design_owner: str | None = None # 전문 상세설계가 따로 필요한 시설(교량 등) — 배치·제원 입력까지만 담당한다. reference_only: bool = False enabled: bool = True diff --git a/B05_Profile/B05_Profile_UI_Structures_Form.ts b/B05_Profile/B05_Profile_UI_Structures_Form.ts index d7410144..f09bc1cf 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Form.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Form.ts @@ -28,6 +28,8 @@ export interface StructuresFormElements { body: HTMLElement; groupSelect: HTMLSelectElement; typeSelect: HTMLSelectElement; + /** 「제원·수량은 다른 화면이 냅니다」 안내 한 줄 — `design_owner` 가 있을 때만 보인다. */ + ownerNote: HTMLElement; startFields: StationFields; anchorFields: StationFields; endFields: StationFields; @@ -96,6 +98,13 @@ export function buildStructuresForm(options: { typeRow.className = "b05-structure__grid"; typeRow.append(field("구조물군", groupSelect), field("종류", typeSelect)); + // 「제원·수량은 다른 화면이 냅니다」 안내 한 줄 — 목록 항목은 [측점][이름]만 적는 규칙이라 + // (2026-08-18 사용자 지시) 표시는 이 폼에 둔다. 측구(옆도랑)가 그 경우다(2026-09-07 사용자: + // 「두되 표시만 해줘」). 문구는 레지스트리 `design_owner` 값으로 만든다. + const ownerNote = document.createElement("p"); + ownerNote.className = "b05-structure__owner-note"; + ownerNote.hidden = true; + // 옵션 칸은 타입마다 다르므로 선택할 때마다 새로 그린다. const optionRow = document.createElement("div"); optionRow.className = "b05-structure__grid"; @@ -129,13 +138,22 @@ export function buildStructuresForm(options: { const positionDivider = document.createElement("hr"); positionDivider.className = "b05-structure__divider"; - body.append(typeRow, positionRow, positionDivider, optionRow, facilityOptions.root, actions); + body.append( + typeRow, + ownerNote, + positionRow, + positionDivider, + optionRow, + facilityOptions.root, + actions, + ); return { root, body, groupSelect, typeSelect, + ownerNote, startFields, anchorFields, endFields, diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index 10c1395d..6363fb7f 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -60,6 +60,7 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu body, groupSelect, typeSelect, + ownerNote, startFields, anchorFields, endFields, @@ -208,6 +209,12 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu startFields.wrap.hidden = !isInterval; endFields.wrap.hidden = !isInterval; primary.disabled = !type; + // 제원·수량 주인이 다른 화면인 타입은 그 사실을 폼에 적는다 — 목록에서 안 보이면 + // 「측구가 왜 없지」로 헤매고, 그렇다고 수량에 넣으면 이중 계상이다(2026-09-07 사용자). + ownerNote.textContent = type?.design_owner + ? `${type.design_owner}에서 관리 — 여기서 넣어도 제원·수량은 그쪽 값을 씁니다.` + : ""; + ownerNote.hidden = !type?.design_owner; anchorFields.wrap.querySelector("span")!.textContent = isInterval ? "기준 측점 (비우면 시작)" : "기준 측점"; diff --git a/B05_Profile/B05_Profile_UI_Style_Structures.css b/B05_Profile/B05_Profile_UI_Style_Structures.css index 0097f181..0339812c 100644 --- a/B05_Profile/B05_Profile_UI_Style_Structures.css +++ b/B05_Profile/B05_Profile_UI_Style_Structures.css @@ -194,6 +194,17 @@ border-top: 1px solid var(--color-border, #3a3f4a); } +/* 「제원·수량은 다른 화면이 냅니다」 안내 — 경고가 아니라 안내라 색은 흐리게, + * 왼쪽 선 하나로만 구분한다(2026-09-07). */ +.b05-structure__owner-note { + margin: 0; + padding: 4px 0 4px 8px; + border-left: 2px solid var(--color-border, #3a3f4a); + color: var(--color-text-muted, #9aa1ad); + font-size: var(--text-caption, 12px); + line-height: 1.5; +} + /* 시작·기준·종료 측점 = 3행. 한 행은 [라벨][측점][+거리] 가로 배치 * (2026-08-17 사용자 지시 2). */ .b05-structure__position-row { diff --git a/common_util/common_util_structure_lengths.py b/common_util/common_util_structure_lengths.py new file mode 100644 index 00000000..092ff85e --- /dev/null +++ b/common_util/common_util_structure_lengths.py @@ -0,0 +1,92 @@ +"""구조물 정본에서 **시설별 연장(m)** 을 낸다 — B군 배수시설 수량의 입력 (계획서 3-6). + +왜 연장만인가 — B군(측구·산마루측구·도수로·맹암거 등)의 수량 단위가 **전부 m** 이다 +(맹암거 12-10 · L형 측구 12-9-1 · 산마루측구 12-9-2 · 소단측구 12-9-3, 지식DB +`04_수량분석정보/배수공_수량.md`). 그래서 단면 기하 없이 구간 길이만으로 셈이 된다. +별표2의 「횡단면도 각 측점 기입 물량」 목록에도 B군은 없어 도면에 그릴 의무가 없다 +(2026-09-07 조사). + +**수량서 양식에 안 묶인다** — 여기서는 종류별 연장·개소만 내고, 어느 코드·어느 칸에 +넣을지는 B08 이 정한다. B08 은 실무 xlsx 양식으로 재작업 예정이라 그 사이에 두는 것이다. +""" + +from pathlib import Path +from typing import Any + +from B05_Profile.B05_Profile_Structures_Repository import load_structures +from B05_Profile.B05_Profile_Structures_Schema import structure_type_map + +# 아직 셈하지 않는 타입 — 계획서 3-9(소단 만들기)가 끝난 뒤에 채운다. 지금 설계에는 +# 소단(berm)이 없어 이 시설이 설 자리 자체가 없다(2026-09-07 조사·사용자 확정). +PENDING_TYPE_IDS = frozenset({"ditch_berm"}) + + +def _merge(spans: list[tuple[float, float]]) -> float: + """겹치는 구간을 합쳐 실제 덮인 길이를 낸다. + + 같은 시설을 겹치게 두 번 넣으면 단순 합은 그 구간을 **두 번 센다**. 연장은 「덮인 + 길이」라 겹침을 지우는 쪽이 맞다. 원래 합(`raw_length_m`)도 함께 내보내므로 입력이 + 겹쳤다는 사실은 숨지 않는다. + """ + total = 0.0 + current_start: float | None = None + current_end = 0.0 + for start, end in sorted(spans): + if current_start is None: + current_start, current_end = start, end + continue + if start <= current_end: + current_end = max(current_end, end) + continue + total += current_end - current_start + current_start, current_end = start, end + if current_start is not None: + total += current_end - current_start + return total + + +def structure_lengths(project_root: str | Path) -> list[dict[str, Any]]: + """구간형 구조물의 시설별 연장을 돌려준다 (기준점 순, 종류별 한 줄). + + 빼는 것 셋 — + · `managed_by` 타입(배관 등): 구조물 정본이 아니라 관 지점 정본 소관이다. + · `design_owner` 타입(측구 = 횡단 설계): 횡단이 이미 터파기 단면적까지 셈하므로 + 여기서 또 세면 **같은 것을 두 번 계상**한다(2026-09-07 사용자 확정). + · `PENDING_TYPE_IDS`(소단측구): 놓일 소단이 아직 없다. + + 시작·종료는 늘 있다 — 구간형은 스키마가 둘 다 없으면 저장을 막는다 + (`StructureInstance.validate_placement_fields`). + """ + types = structure_type_map() + spans: dict[str, list[tuple[float, float]]] = {} + counts: dict[str, int] = {} + + for structure in load_structures(str(project_root))[1]: + definition = types.get(structure.type_id) + if definition is None or definition.placement != "interval": + continue + if definition.managed_by or definition.design_owner: + continue + if structure.type_id in PENDING_TYPE_IDS: + continue + start, end = float(structure.start_m), float(structure.end_m) + counts[structure.type_id] = counts.get(structure.type_id, 0) + 1 + spans.setdefault(structure.type_id, []).append((min(start, end), max(start, end))) + + rows: list[dict[str, Any]] = [] + for type_id, count in counts.items(): + entries = spans[type_id] + rows.append( + { + "type_id": type_id, + "group": types[type_id].group, + "name": types[type_id].name, + "count": count, + # 겹침을 지운 실제 연장 — 수량서에 쓸 값. + "length_m": round(_merge(entries), 2), + # 입력한 구간 길이의 단순 합 — 위와 다르면 구간이 겹쳐 있다는 뜻. + "raw_length_m": round(sum(end - start for start, end in entries), 2), + } + ) + rows.sort(key=lambda row: (row["group"], row["name"])) + return rows