feat(b08): 산림 형태 미판정 14 → 0 — 안티그래비티 원문 대조 판정을 자료에 넣음 · 밑수 5도 함께
브레인 일감 1. `산림품셈_form_미판정_14개_원문대조_규명.json`(안티그래비티)을 기존
사람 판정 자리(`FORM_JUDGMENTS`)에 실음 — 머리 소실 9 · 혼합 3 · 공식 1 · 글값 1, 전부 금액 대상.
- 형태 — 소요량형 9 · 생산량형 3 · 계수 2. **산림 미판정이 0 이 됨**
⚠ 안티그래비티가 적은 `formula` 는 낱말을 새로 만들지 않고 뜻이 같은 `coefficient` 로 둠
(받는 쪽이 모르는 형태를 만나면 조용히 건너뜀)
- 밑수 — 머리가 소실돼 **자동으로는 못 읽는 표 일곱**만 `BASIS_JUDGMENTS` 로(막갈이 100㎡당 ·
묘목운반 1,000본당 · 12-1-3 ㎥당 · 12-11-1 m당). 자동으로 읽히는 표는 안 넣음(정본이 흐려짐)
- **더하기만** — 형태 14가 undetermined 에서 갈렸고 밑수 5가 빈칸에서 찼음. 덮어쓴 값 0
- 딸린 시험·이름표 손질 — F0349 는 이제 소요량형 · F0322 「형식 미정」 사유가 사라짐 ·
미판정 목록이 비어 이름표에서 표 → 값 묶음으로 내려감(다시 생기면 표로 섬)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsGw1Dz9HmhuAxGisxmDPF
This commit is contained in:
@@ -5,7 +5,8 @@
|
||||
|
||||
⚠⚠ 뒤집히면 20배 틀림 — `productivity`(1÷값) / `requirement`(값÷밑수)는 **표 머리와 표 바로 위
|
||||
본문의 단위 표현**으로만 가른다. 행 이름·비고의 직종 이름에 끌려 읽지 않는다.
|
||||
⚠ **못 가른 표는 싣지 않는다** — undetermined 로 남는다(막갈이 깊이표·드론운반 인원 등 밑수 불명).
|
||||
⚠ **못 가른 표는 싣지 않는다** — undetermined 로 남는다.
|
||||
2026-09-17 안티그래비티 원문 대조로 **마지막 14표를 가름** — 산림 미판정이 0 이 됐다.
|
||||
⚠ `coefficient`·`reference` 로 가른 표는 일위대가 줄로 안 읽힌다(자원 축이 건너뜀) — 시공능력
|
||||
공식 계수는 형태와 상관없이 공식 읽기가 표를 직접 본다.
|
||||
⚠ 표 번호는 원천 차례로 매긴 것이라 **절 번호를 함께 대조**한다 — 어긋나면 빌드를 멈춘다.
|
||||
@@ -151,9 +152,74 @@ FORM_JUDGMENTS: dict[str, tuple[str, str, str]] = {
|
||||
"F0429": ("13-10-2", "coefficient", "[주]⑥ 관입률별 계수"),
|
||||
"F0439": ("13-12-2", "coefficient", "[주]⑦ 높이·기울기 할증(%)"),
|
||||
"F0446": ("13-15-2", "reference", "작업조건(어려운·쉬운) 구분 기준"),
|
||||
# ── 미판정 14 — 2026-09-17 안티그래비티 원문 대조 규명(브레인 넘김) ─────────────
|
||||
# `산림품셈_form_미판정_14개_원문대조_규명.json` · 머리 소실 9 · 혼합 3 · 공식 1 · 글값 1.
|
||||
# ⚠ 여기까지가 산림 미판정 전부다 — 다 채우면 `form_undetermined` 가 0 이 된다.
|
||||
"F0140": (
|
||||
"5-26-1",
|
||||
"requirement",
|
||||
"머리 소실 — 윗 2줄 막갈이깊이(cm)를 열 머리로 되살림, 토성×깊이별 보통인부(100㎡당)",
|
||||
),
|
||||
"F0141": ("5-26-2", "requirement", "위와 같은 모양(중갈이) — 토성×깊이별 보통인부(100㎡당)"),
|
||||
"F0142": ("5-26-3", "requirement", "위와 같은 모양(전면갈이) — 토성×깊이별 보통인부(100㎡당)"),
|
||||
"F0195": (
|
||||
"7-14",
|
||||
"productivity",
|
||||
"머리 소실 — 값이 「1인 1일 공정량」이라 생산량형(품 = 1 ÷ 값)",
|
||||
),
|
||||
"F0248": ("9-7-1", "coefficient", "머리 소실 — 값이 품이 아니라 계산식 파라미터"),
|
||||
"F0322": ("10-13-1", "requirement", "머리 소실 — 묘목 1,000본당 소요량"),
|
||||
"F0323": ("10-13-2", "requirement", "위와 같음(묘목 1,000본당)"),
|
||||
"F0333": ("12-1-3", "requirement", "머리 소실 — ㎥당 소요량"),
|
||||
"F0346": ("12-11-1", "requirement", "머리 소실 — m당 소요량"),
|
||||
"F0169": ("7-1-1", "productivity", "혼합 — 값이 「㎥/인·일」이라 생산량형"),
|
||||
"F0304": ("10-6-3", "requirement", "혼합 — 소요량형(밑수는 표 밖이라 미확보로 남김)"),
|
||||
"F0356": ("12-17-1", "productivity", "혼합 — 값이 「㎥/일」이라 생산량형"),
|
||||
# ⚠ 안티그래비티는 이 표를 `formula` 로 적었으나 **낱말을 새로 만들지 않는다** —
|
||||
# 받는 쪽이 모르는 형태를 만나면 조용히 건너뛴다. 뜻이 같은 `coefficient`(계산식 파라미터)로 둔다.
|
||||
"F0257": (
|
||||
"9-11-1",
|
||||
"coefficient",
|
||||
"공식 명세 — 시공능력 공식 파라미터(안티그래비티 판정 `formula`)",
|
||||
),
|
||||
"F0349": (
|
||||
"12-11-3",
|
||||
"requirement",
|
||||
"글값 — 값이 글로 적힌 소요량형(밑수는 표 밖이라 미확보로 남김)",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
#: 표 번호 → (절 번호, 밑수 수, 밑수 단위, 까닭).
|
||||
#: **머리가 소실돼 자동으로는 못 읽는 표**의 밑수만 싣는다 — 원문 대조로 확인한 것만.
|
||||
#: ⚠ 자동으로 읽히는 표는 여기 넣지 않는다. 두 곳에서 값을 매기면 어느 쪽이 정본인지 흐려진다.
|
||||
#: 근거 — `resources/knowledge/technical_info/01_임도/05_원가정보/
|
||||
#: 산림품셈_form_미판정_14개_원문대조_규명.json` (2026-09-17 안티그래비티 원문 대조).
|
||||
BASIS_JUDGMENTS: dict[str, tuple[str, float, str, str]] = {
|
||||
"F0140": ("5-26-1", 100.0, "㎡", "막갈이 — 머리 소실 · 원문 「100㎡당」"),
|
||||
"F0141": ("5-26-2", 100.0, "㎡", "중갈이 — 위와 같음"),
|
||||
"F0142": ("5-26-3", 100.0, "㎡", "전면갈이 — 위와 같음"),
|
||||
"F0322": ("10-13-1", 1000.0, "본", "묘목 운반 — 원문 「묘목 1,000본당」"),
|
||||
"F0323": ("10-13-2", 1000.0, "본", "위와 같음"),
|
||||
"F0333": ("12-1-3", 1.0, "㎥", "원문 「㎥당」"),
|
||||
"F0346": ("12-11-1", 1.0, "m", "원문 「m당」"),
|
||||
}
|
||||
|
||||
|
||||
def judged_basis(table_id: str, number: str | None) -> tuple[float, str, str] | None:
|
||||
"""사람이 확인한 `(밑수 수, 단위, 까닭)`. 없으면 `None`. 절 번호가 어긋나면 멈춘다."""
|
||||
found = BASIS_JUDGMENTS.get(table_id)
|
||||
if found is None:
|
||||
return None
|
||||
section, quantity, unit, why = found
|
||||
if section != number:
|
||||
raise ValueError(
|
||||
f"밑수 판정 {table_id} 의 절 {section} 이 원천의 {number} 와 다릅니다 — "
|
||||
"원천 표 차례가 바뀌었으니 판정을 다시 대조할 것"
|
||||
)
|
||||
return quantity, unit, f"사람 판정 — {why}"
|
||||
|
||||
|
||||
def judged_form(table_id: str, number: str | None) -> tuple[str, str] | None:
|
||||
"""사람이 가른 `(형태, 까닭)`. 없으면 `None`. 절 번호가 어긋나면 멈춘다."""
|
||||
found = FORM_JUDGMENTS.get(table_id)
|
||||
|
||||
@@ -10,7 +10,7 @@ from __future__ import annotations
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Forms import judged_form
|
||||
from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Forms import judged_basis, judged_form
|
||||
|
||||
# ── 형태 판정 ────────────────────────────────────────────────────────────
|
||||
# 헤더·비고 문자열에서 찾는 표지. 앞의 것이 먼저 걸린다(생산량형이 더 좁은 표현이라 우선).
|
||||
@@ -576,13 +576,18 @@ def table_entry(
|
||||
# ⚠ **본문이 정본이다.** 표 안을 긁는 쪽은 보조 — 비고에 적힌 다른 기준
|
||||
# (「10㎡당」 같은 참고 문구)을 그 표의 밑수로 잘못 물어 온다.
|
||||
# 실제로 13-3-1 이 본문 `(단위: ㎥당)` 인데 표 안 긁기가 `10㎡` 를 물어 왔다.
|
||||
basis_qty = basis_unit = None
|
||||
if source_lines:
|
||||
basis_qty = basis_unit = basis_source = None
|
||||
# 사람이 원문에서 확인한 밑수가 가장 앞이다 — **머리가 소실돼 자동으로는 못 읽는 표**만 해당.
|
||||
if judged := judged_basis(table["table_id"], number):
|
||||
basis_qty, basis_unit, basis_source = judged[0], judged[1], judged[2]
|
||||
if basis_unit is None and source_lines:
|
||||
basis_qty, basis_unit = basis_from_source(source_lines, int(table.get("line") or 0))
|
||||
elif basis_unit is None:
|
||||
basis_source = None
|
||||
if basis_qty is None and basis_unit is None:
|
||||
basis_qty, basis_unit = detect_basis(table)
|
||||
basis_source = "표 안" if basis_unit else None
|
||||
else:
|
||||
elif basis_source is None:
|
||||
basis_source = "본문"
|
||||
# 본문·표 안에 없을 때만 **절 이름**을 본다(4-2-2 「1,000㎡당」) — 이름 전체가 밑수일 때만.
|
||||
if basis_unit is None and node_name is not None:
|
||||
|
||||
@@ -1976,12 +1976,12 @@
|
||||
"merged_tables": 17,
|
||||
"merged_columns": 178,
|
||||
"files": 41,
|
||||
"tables": 101,
|
||||
"columns": 540,
|
||||
"value_groups": 203,
|
||||
"tables": 100,
|
||||
"columns": 535,
|
||||
"value_groups": 204,
|
||||
"unknown": 0,
|
||||
"value_groups_by_kind": {
|
||||
"doc": 137,
|
||||
"doc": 138,
|
||||
"value": 66
|
||||
}
|
||||
},
|
||||
@@ -6327,7 +6327,7 @@
|
||||
"name_ko": "밑수 못 찾은 표",
|
||||
"summary": "기준 수량을 못 읽은 품셈 표. 1 단위당으로 단정하면 곱셈이 틀린다.",
|
||||
"shape": "list",
|
||||
"rows": 99,
|
||||
"rows": 103,
|
||||
"columns": [
|
||||
{
|
||||
"key": "pum_table_id",
|
||||
@@ -6369,55 +6369,21 @@
|
||||
"file_id": "form_undetermined",
|
||||
"path": "resources/data_work_item_master/form_undetermined_2026-01-01.json",
|
||||
"name_ko": "형태 못 정한 표",
|
||||
"summary": "품·소요량·계수 어느 형태인지 못 정한 품셈 표 기록. 정본이 아니다.",
|
||||
"summary": "품·소요량·계수 어느 형태인지 못 정한 품셈 표 기록. 정본이 아니다. 2026-09-17 산림은 **비었음** — 열넷을 다 가름(다시 생기면 표가 선다).",
|
||||
"kind": "byproduct",
|
||||
"tables": [
|
||||
{
|
||||
"key": "items",
|
||||
"name_ko": "형태 못 정한 표",
|
||||
"summary": "품·소요량·계수 어느 형태인지 사람이 봐야 하는 표.",
|
||||
"shape": "list",
|
||||
"rows": 14,
|
||||
"columns": [
|
||||
{
|
||||
"key": "pum_table_id",
|
||||
"name_ko": "품셈 표 번호",
|
||||
"unit": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"key": "section",
|
||||
"name_ko": "품셈 절",
|
||||
"unit": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"key": "headers",
|
||||
"name_ko": "표 머리",
|
||||
"unit": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"key": "first_rows",
|
||||
"name_ko": "첫 줄들",
|
||||
"unit": "",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"key": "reason",
|
||||
"name_ko": "사유",
|
||||
"unit": "",
|
||||
"visible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tables": [],
|
||||
"value_groups": [
|
||||
{
|
||||
"key": "note",
|
||||
"name_ko": "설명",
|
||||
"summary": "이 벌이 무엇이고 어떻게 쓰는지 적은 글.",
|
||||
"kind": "doc"
|
||||
},
|
||||
{
|
||||
"key": "items",
|
||||
"name_ko": "형태 미판정 표",
|
||||
"summary": "2026-09-17 산림은 비었음 — 열넷을 다 가름. 다시 생기면 표로 선다.",
|
||||
"kind": "doc"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"dataset_id": "data_work_item_master_manifest",
|
||||
"generated_at": "2026-09-17T18:29:07+09:00",
|
||||
"generated_at": "2026-09-17T19:00:55+09:00",
|
||||
"built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py",
|
||||
"source": {
|
||||
"dataset_id": "pum_forest",
|
||||
@@ -12,18 +12,18 @@
|
||||
"files": [
|
||||
{
|
||||
"file": "work_item_master_2026-01-01.json",
|
||||
"sha256": "ef9a9d0424a2b602b74640e6bdb49da3ccdd31c873367d404a09d1ede7efd882",
|
||||
"size_bytes": 925160
|
||||
"sha256": "e4012dc2cbdd0dc74587eeb99dd14581c53e8457e8c678259ff91c3549a0739c",
|
||||
"size_bytes": 925871
|
||||
},
|
||||
{
|
||||
"file": "form_undetermined_2026-01-01.json",
|
||||
"sha256": "6b3476810ddee395856b997fa524a0410a5001fe47511b989c1db2feb82dccd2",
|
||||
"size_bytes": 6789
|
||||
"sha256": "973ea53279095a574157daf5fa13d3b35379af1c54df5debeee7f3ed934d943e",
|
||||
"size_bytes": 245
|
||||
},
|
||||
{
|
||||
"file": "basis_missing_2026-01-01.json",
|
||||
"sha256": "15306da88ffa36bbe33b65c0f290fcaab94888e494e61546b1b6a2949d814a90",
|
||||
"size_bytes": 13680
|
||||
"sha256": "13b663780bca6143a126a2a873ff31b0a9eeb2aa41ea89e928b9c6a23537b164",
|
||||
"size_bytes": 14212
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"dataset_id": "data_work_item_master_manifest",
|
||||
"generated_at": "2026-09-17T18:29:08+09:00",
|
||||
"generated_at": "2026-09-17T19:00:56+09:00",
|
||||
"built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py",
|
||||
"source": {
|
||||
"dataset_id": "pum_const",
|
||||
@@ -12,7 +12,7 @@
|
||||
"files": [
|
||||
{
|
||||
"file": "const_work_item_master_2026-01-01.json",
|
||||
"sha256": "f81019fc9699d01c074f55fc1e8074d16ce7b0a81c0291fcbb63fb5094b3c9b5",
|
||||
"sha256": "170abf1f23e7265415d460659b7552aa83c9136dc93b45344277c1e3c56ba6ed",
|
||||
"size_bytes": 3780743
|
||||
},
|
||||
{
|
||||
|
||||
@@ -202,6 +202,12 @@
|
||||
"pum_form": "requirement",
|
||||
"line": 3244
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0169",
|
||||
"section": "7-1-1. 수확",
|
||||
"pum_form": "productivity",
|
||||
"line": 3361
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0170",
|
||||
"section": "7-1-1. 수확",
|
||||
@@ -292,6 +298,12 @@
|
||||
"pum_form": "requirement",
|
||||
"line": 3763
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0195",
|
||||
"section": "7-14. 원목 운반-수확",
|
||||
"pum_form": "productivity",
|
||||
"line": 3849
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0196",
|
||||
"section": "7-15-1. 수확",
|
||||
@@ -472,6 +484,12 @@
|
||||
"pum_form": "requirement",
|
||||
"line": 5512
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0304",
|
||||
"section": "10-6-3. 기타 임업자재",
|
||||
"pum_form": "requirement",
|
||||
"line": 5669
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0310",
|
||||
"section": "10-7-4. 모노레일 운반",
|
||||
@@ -538,6 +556,12 @@
|
||||
"pum_form": "requirement",
|
||||
"line": 6499
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0356",
|
||||
"section": "12-2 표면 마무리를 따른다.",
|
||||
"pum_form": "productivity",
|
||||
"line": 6520
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0362",
|
||||
"section": "12-2 표면 마무리를 따른다.",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"effective_date": "2026-01-01",
|
||||
"pum_edition": "2026-01-01",
|
||||
"toc_edition": "2026-01-01",
|
||||
"generated_at": "2026-09-17T18:29:08+09:00",
|
||||
"generated_at": "2026-09-17T19:00:56+09:00",
|
||||
"dataset_version": {
|
||||
"dataset_id": "pum_const",
|
||||
"effective_date": "2026-01-01",
|
||||
|
||||
@@ -3,394 +3,5 @@
|
||||
"dataset_id": "work_item_master_form_undetermined",
|
||||
"effective_date": "2026-01-01",
|
||||
"note": "형태를 못 정한 표. 사람이 보고 productivity/requirement/coefficient 로 확정할 것.",
|
||||
"items": [
|
||||
{
|
||||
"pum_table_id": "F0140",
|
||||
"section": "5-26-1. 막갈이",
|
||||
"headers": [
|
||||
"토성",
|
||||
"막갈이깊이(cm)"
|
||||
],
|
||||
"first_rows": [
|
||||
[
|
||||
"9",
|
||||
"12",
|
||||
"15",
|
||||
"18",
|
||||
"21",
|
||||
""
|
||||
],
|
||||
[
|
||||
"사토",
|
||||
"5",
|
||||
"7",
|
||||
"9",
|
||||
"11",
|
||||
"13"
|
||||
]
|
||||
],
|
||||
"reason": "헤더·첫 행에 단위·밑수·직종 표지 없음"
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0141",
|
||||
"section": "5-26-2. 흙부수기",
|
||||
"headers": [
|
||||
"토성",
|
||||
"막갈이깊이(cm)"
|
||||
],
|
||||
"first_rows": [
|
||||
[
|
||||
"9",
|
||||
"12",
|
||||
"15",
|
||||
"18",
|
||||
"21",
|
||||
""
|
||||
],
|
||||
[
|
||||
"사토",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7"
|
||||
]
|
||||
],
|
||||
"reason": "헤더·첫 행에 단위·밑수·직종 표지 없음"
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0142",
|
||||
"section": "5-26-3. 돌자갈치우기",
|
||||
"headers": [
|
||||
"토성",
|
||||
"경토깊이(cm)"
|
||||
],
|
||||
"first_rows": [
|
||||
[
|
||||
"10% 이내",
|
||||
"10∼30%",
|
||||
"30% 이상",
|
||||
""
|
||||
],
|
||||
[
|
||||
"개답",
|
||||
"2",
|
||||
"6",
|
||||
"17"
|
||||
]
|
||||
],
|
||||
"reason": "헤더·첫 행에 단위·밑수·직종 표지 없음"
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0169",
|
||||
"section": "7-1-1. 수확",
|
||||
"headers": [
|
||||
"구 분",
|
||||
"어려움 (15˚ 미만)",
|
||||
"중 (30˚ 초과)",
|
||||
"쉬 움 (15˚~30˚)"
|
||||
],
|
||||
"first_rows": [
|
||||
[
|
||||
"",
|
||||
"㎥",
|
||||
"㎥",
|
||||
"㎥"
|
||||
],
|
||||
[
|
||||
"100m이하",
|
||||
"1.9",
|
||||
"3.1",
|
||||
"4.3"
|
||||
]
|
||||
],
|
||||
"reason": "헤더·첫 행에 단위·밑수·직종 표지 없음"
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0195",
|
||||
"section": "7-14. 원목 운반-수확",
|
||||
"headers": [
|
||||
"종 별",
|
||||
"공 정"
|
||||
],
|
||||
"first_rows": [
|
||||
[
|
||||
"1회 적 재 량",
|
||||
"1인 1일공정",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
],
|
||||
[
|
||||
"용 재 (원목)",
|
||||
"신 재 (활 잡)",
|
||||
"목 탄",
|
||||
"운 반",
|
||||
"상 하 차",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
]
|
||||
],
|
||||
"reason": "헤더·첫 행에 단위·밑수·직종 표지 없음"
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0248",
|
||||
"section": "9-7-1. T=30㎝ 미만",
|
||||
"headers": [
|
||||
"구 분",
|
||||
"적 용"
|
||||
],
|
||||
"first_rows": [
|
||||
[
|
||||
"K",
|
||||
"0.55"
|
||||
],
|
||||
[
|
||||
"f",
|
||||
"1/1.5(0.67)"
|
||||
]
|
||||
],
|
||||
"reason": "헤더·첫 행에 단위·밑수·직종 표지 없음"
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0257",
|
||||
"section": "9-11-1. 콘크리트(기계)",
|
||||
"headers": [
|
||||
"구 분",
|
||||
"단 위",
|
||||
"적 용",
|
||||
"비 고"
|
||||
],
|
||||
"first_rows": [
|
||||
[
|
||||
"거리(L)",
|
||||
"km",
|
||||
"0.1",
|
||||
"N=(V×T)/(120×L+V.t)= 회/일 Q = N×q = ℓ/일"
|
||||
],
|
||||
[
|
||||
"운반속도(V)",
|
||||
"km/hr",
|
||||
"2.5",
|
||||
""
|
||||
]
|
||||
],
|
||||
"reason": "헤더·첫 행에 단위·밑수·직종 표지 없음"
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0304",
|
||||
"section": "10-6-3. 기타 임업자재",
|
||||
"headers": [
|
||||
"종 별 거 리",
|
||||
"목 재",
|
||||
"볏 짚",
|
||||
"섶단 · 새",
|
||||
"나뭇 가지 단",
|
||||
"자른 떼 (20㎝ ×20㎝)",
|
||||
"식생낭 (혼토입)",
|
||||
"편책용 말뚝 (목재, 파이프)",
|
||||
"볏짚멍석 (종비포함)",
|
||||
"비 료",
|
||||
"비 고"
|
||||
],
|
||||
"first_rows": [
|
||||
[
|
||||
"소 재 (조재목)",
|
||||
"제재",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
],
|
||||
[
|
||||
"m",
|
||||
"인/㎥",
|
||||
"인/㎥",
|
||||
"인/ 100속",
|
||||
"인/ 100속",
|
||||
"인/ 100속",
|
||||
"인/ 100매",
|
||||
"인/ 100개",
|
||||
"인/ 100본",
|
||||
"인/ 1000㎡",
|
||||
"인/ 톤",
|
||||
""
|
||||
]
|
||||
],
|
||||
"reason": "헤더·첫 행에 단위·밑수·직종 표지 없음"
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0322",
|
||||
"section": "10-13-1. 드론운반",
|
||||
"headers": [
|
||||
"수평거리",
|
||||
"100m",
|
||||
"200m",
|
||||
"300m",
|
||||
"400m",
|
||||
"500m",
|
||||
"600m"
|
||||
],
|
||||
"first_rows": [
|
||||
[
|
||||
"인원(명)",
|
||||
"0.29",
|
||||
"0.37",
|
||||
"0.46",
|
||||
"0.54",
|
||||
"0.62",
|
||||
"0.70"
|
||||
]
|
||||
],
|
||||
"reason": "헤더·첫 행에 단위·밑수·직종 표지 없음"
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0323",
|
||||
"section": "10-13-2. 인력운반",
|
||||
"headers": [
|
||||
"수평거리",
|
||||
"100m",
|
||||
"200m",
|
||||
"300m",
|
||||
"400m",
|
||||
"500m",
|
||||
"600m"
|
||||
],
|
||||
"first_rows": [
|
||||
[
|
||||
"인원(명)",
|
||||
"0.67",
|
||||
"0.73",
|
||||
"0.79",
|
||||
"0.85",
|
||||
"0.92",
|
||||
"0.99"
|
||||
]
|
||||
],
|
||||
"reason": "헤더·첫 행에 단위·밑수·직종 표지 없음"
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0333",
|
||||
"section": "12-1-3. 인력비빔타설",
|
||||
"headers": [
|
||||
"온도 품종",
|
||||
"00C때",
|
||||
"-50C때",
|
||||
"-100C때",
|
||||
"-200C때"
|
||||
],
|
||||
"first_rows": [
|
||||
[
|
||||
"25",
|
||||
"(A)",
|
||||
"357",
|
||||
"893",
|
||||
"931"
|
||||
],
|
||||
[
|
||||
"(B)",
|
||||
"346",
|
||||
"828",
|
||||
"1,011",
|
||||
""
|
||||
]
|
||||
],
|
||||
"reason": "헤더·첫 행에 단위·밑수·직종 표지 없음"
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0346",
|
||||
"section": "12-11-1. VR관(소켓식)",
|
||||
"headers": [
|
||||
"구 분",
|
||||
"규격",
|
||||
"단위",
|
||||
"관 경 별 적 용",
|
||||
"비 고"
|
||||
],
|
||||
"first_rows": [
|
||||
[
|
||||
"∅800mm",
|
||||
"∅1000mm",
|
||||
"∅1200mm",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
],
|
||||
[
|
||||
"VR관",
|
||||
"",
|
||||
"m",
|
||||
"1.0",
|
||||
"1.0",
|
||||
"1.0",
|
||||
"별도계산"
|
||||
]
|
||||
],
|
||||
"reason": "헤더·첫 행에 단위·밑수·직종 표지 없음"
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0349",
|
||||
"section": "12-11-3. 파형강관",
|
||||
"headers": [
|
||||
"구 분",
|
||||
"규격",
|
||||
"단위",
|
||||
"관 경 별 적 용",
|
||||
"비 고"
|
||||
],
|
||||
"first_rows": [
|
||||
[
|
||||
"∅800mm",
|
||||
"∅1000mm",
|
||||
"∅1200mm",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
],
|
||||
[
|
||||
"파형강관",
|
||||
"",
|
||||
"m",
|
||||
"1.0",
|
||||
"1.0",
|
||||
"1.0",
|
||||
"별산"
|
||||
]
|
||||
],
|
||||
"reason": "헤더·첫 행에 단위·밑수·직종 표지 없음"
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0356",
|
||||
"section": "12-17-1. 펌프카 타설",
|
||||
"headers": [
|
||||
"슬 럼 프",
|
||||
"기준 시공량"
|
||||
],
|
||||
"first_rows": [
|
||||
[
|
||||
"무근콘크리트",
|
||||
"철근콘크리트",
|
||||
""
|
||||
],
|
||||
[
|
||||
"8 ~ 12 cm",
|
||||
"130",
|
||||
"125"
|
||||
]
|
||||
],
|
||||
"reason": "헤더·첫 행에 단위·밑수·직종 표지 없음"
|
||||
}
|
||||
]
|
||||
"items": []
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"effective_date": "2026-01-01",
|
||||
"pum_edition": "2026-01-01",
|
||||
"toc_edition": "산림청고시제2025-82호",
|
||||
"generated_at": "2026-09-17T18:29:07+09:00",
|
||||
"generated_at": "2026-09-17T19:00:55+09:00",
|
||||
"dataset_version": {
|
||||
"dataset_id": "pum_forest",
|
||||
"effective_date": "2026-01-01",
|
||||
@@ -24,10 +24,10 @@
|
||||
"tables_total": 475,
|
||||
"tables_attached": 453,
|
||||
"tables_orphan": 22,
|
||||
"form_undetermined": 14,
|
||||
"basis_found": 222,
|
||||
"basis_missing": 99,
|
||||
"basis_grouped": 49,
|
||||
"form_undetermined": 0,
|
||||
"basis_found": 227,
|
||||
"basis_missing": 103,
|
||||
"basis_grouped": 54,
|
||||
"axis_roles": {
|
||||
"general_provision": 78,
|
||||
"group": 85,
|
||||
@@ -11542,11 +11542,11 @@
|
||||
"pum_table_id": "F0140",
|
||||
"section": "5-26-1. 막갈이",
|
||||
"source_line": 2874,
|
||||
"pum_form": "undetermined",
|
||||
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
|
||||
"basis_quantity": null,
|
||||
"basis_unit": null,
|
||||
"basis_source": null,
|
||||
"pum_form": "requirement",
|
||||
"form_basis": "사람 판정 — 머리 소실 — 윗 2줄 막갈이깊이(cm)를 열 머리로 되살림, 토성×깊이별 보통인부(100㎡당)",
|
||||
"basis_quantity": 100.0,
|
||||
"basis_unit": "㎡",
|
||||
"basis_source": "사람 판정 — 막갈이 — 머리 소실 · 원문 「100㎡당」",
|
||||
"resource_shares": {},
|
||||
"partial_ratio": false,
|
||||
"expression_cells": [],
|
||||
@@ -11617,11 +11617,11 @@
|
||||
"pum_table_id": "F0141",
|
||||
"section": "5-26-2. 흙부수기",
|
||||
"source_line": 2886,
|
||||
"pum_form": "undetermined",
|
||||
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
|
||||
"basis_quantity": null,
|
||||
"basis_unit": null,
|
||||
"basis_source": null,
|
||||
"pum_form": "requirement",
|
||||
"form_basis": "사람 판정 — 위와 같은 모양(중갈이) — 토성×깊이별 보통인부(100㎡당)",
|
||||
"basis_quantity": 100.0,
|
||||
"basis_unit": "㎡",
|
||||
"basis_source": "사람 판정 — 중갈이 — 위와 같음",
|
||||
"resource_shares": {},
|
||||
"partial_ratio": false,
|
||||
"expression_cells": [],
|
||||
@@ -11692,11 +11692,11 @@
|
||||
"pum_table_id": "F0142",
|
||||
"section": "5-26-3. 돌자갈치우기",
|
||||
"source_line": 2897,
|
||||
"pum_form": "undetermined",
|
||||
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
|
||||
"basis_quantity": null,
|
||||
"basis_unit": null,
|
||||
"basis_source": null,
|
||||
"pum_form": "requirement",
|
||||
"form_basis": "사람 판정 — 위와 같은 모양(전면갈이) — 토성×깊이별 보통인부(100㎡당)",
|
||||
"basis_quantity": 100.0,
|
||||
"basis_unit": "㎡",
|
||||
"basis_source": "사람 판정 — 전면갈이 — 위와 같음",
|
||||
"resource_shares": {},
|
||||
"partial_ratio": false,
|
||||
"expression_cells": [],
|
||||
@@ -13595,8 +13595,8 @@
|
||||
"pum_table_id": "F0169",
|
||||
"section": "7-1-1. 수확",
|
||||
"source_line": 3361,
|
||||
"pum_form": "undetermined",
|
||||
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
|
||||
"pum_form": "productivity",
|
||||
"form_basis": "사람 판정 — 혼합 — 값이 「㎥/인·일」이라 생산량형",
|
||||
"basis_quantity": null,
|
||||
"basis_unit": null,
|
||||
"basis_source": null,
|
||||
@@ -15953,8 +15953,8 @@
|
||||
"pum_table_id": "F0195",
|
||||
"section": "7-14. 원목 운반-수확",
|
||||
"source_line": 3849,
|
||||
"pum_form": "undetermined",
|
||||
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
|
||||
"pum_form": "productivity",
|
||||
"form_basis": "사람 판정 — 머리 소실 — 값이 「1인 1일 공정량」이라 생산량형(품 = 1 ÷ 값)",
|
||||
"basis_quantity": null,
|
||||
"basis_unit": null,
|
||||
"basis_source": null,
|
||||
@@ -21726,8 +21726,8 @@
|
||||
"pum_table_id": "F0248",
|
||||
"section": "9-7-1. T=30㎝ 미만",
|
||||
"source_line": 4817,
|
||||
"pum_form": "undetermined",
|
||||
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
|
||||
"pum_form": "coefficient",
|
||||
"form_basis": "사람 판정 — 머리 소실 — 값이 품이 아니라 계산식 파라미터",
|
||||
"basis_quantity": null,
|
||||
"basis_unit": null,
|
||||
"basis_source": null,
|
||||
@@ -22314,8 +22314,8 @@
|
||||
"pum_table_id": "F0257",
|
||||
"section": "9-11-1. 콘크리트(기계)",
|
||||
"source_line": 4961,
|
||||
"pum_form": "undetermined",
|
||||
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
|
||||
"pum_form": "coefficient",
|
||||
"form_basis": "사람 판정 — 공식 명세 — 시공능력 공식 파라미터(안티그래비티 판정 `formula`)",
|
||||
"basis_quantity": null,
|
||||
"basis_unit": null,
|
||||
"basis_source": null,
|
||||
@@ -26404,8 +26404,8 @@
|
||||
"pum_table_id": "F0304",
|
||||
"section": "10-6-3. 기타 임업자재",
|
||||
"source_line": 5669,
|
||||
"pum_form": "undetermined",
|
||||
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
|
||||
"pum_form": "requirement",
|
||||
"form_basis": "사람 판정 — 혼합 — 소요량형(밑수는 표 밖이라 미확보로 남김)",
|
||||
"basis_quantity": null,
|
||||
"basis_unit": null,
|
||||
"basis_source": null,
|
||||
@@ -27802,11 +27802,11 @@
|
||||
"pum_table_id": "F0322",
|
||||
"section": "10-13-1. 드론운반",
|
||||
"source_line": 6003,
|
||||
"pum_form": "undetermined",
|
||||
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
|
||||
"basis_quantity": null,
|
||||
"basis_unit": null,
|
||||
"basis_source": null,
|
||||
"pum_form": "requirement",
|
||||
"form_basis": "사람 판정 — 머리 소실 — 묘목 1,000본당 소요량",
|
||||
"basis_quantity": 1000.0,
|
||||
"basis_unit": "본",
|
||||
"basis_source": "사람 판정 — 묘목 운반 — 원문 「묘목 1,000본당」",
|
||||
"resource_shares": {},
|
||||
"partial_ratio": false,
|
||||
"expression_cells": [],
|
||||
@@ -27859,11 +27859,11 @@
|
||||
"pum_table_id": "F0323",
|
||||
"section": "10-13-2. 인력운반",
|
||||
"source_line": 6009,
|
||||
"pum_form": "undetermined",
|
||||
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
|
||||
"basis_quantity": null,
|
||||
"basis_unit": null,
|
||||
"basis_source": null,
|
||||
"pum_form": "requirement",
|
||||
"form_basis": "사람 판정 — 위와 같음(묘목 1,000본당)",
|
||||
"basis_quantity": 1000.0,
|
||||
"basis_unit": "본",
|
||||
"basis_source": "사람 판정 — 위와 같음",
|
||||
"resource_shares": {},
|
||||
"partial_ratio": false,
|
||||
"expression_cells": [],
|
||||
@@ -28706,11 +28706,11 @@
|
||||
"pum_table_id": "F0333",
|
||||
"section": "12-1-3. 인력비빔타설",
|
||||
"source_line": 6145,
|
||||
"pum_form": "undetermined",
|
||||
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
|
||||
"pum_form": "requirement",
|
||||
"form_basis": "사람 판정 — 머리 소실 — ㎥당 소요량",
|
||||
"basis_quantity": 1.0,
|
||||
"basis_unit": "㎥",
|
||||
"basis_source": "본문",
|
||||
"basis_source": "사람 판정 — 원문 「㎥당」",
|
||||
"resource_shares": {},
|
||||
"partial_ratio": false,
|
||||
"expression_cells": [],
|
||||
@@ -29954,11 +29954,11 @@
|
||||
"pum_table_id": "F0346",
|
||||
"section": "12-11-1. VR관(소켓식)",
|
||||
"source_line": 6360,
|
||||
"pum_form": "undetermined",
|
||||
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
|
||||
"pum_form": "requirement",
|
||||
"form_basis": "사람 판정 — 머리 소실 — m당 소요량",
|
||||
"basis_quantity": 1.0,
|
||||
"basis_unit": "m",
|
||||
"basis_source": "본문",
|
||||
"basis_source": "사람 판정 — 원문 「m당」",
|
||||
"resource_shares": {},
|
||||
"partial_ratio": false,
|
||||
"expression_cells": [
|
||||
@@ -30328,8 +30328,8 @@
|
||||
"pum_table_id": "F0349",
|
||||
"section": "12-11-3. 파형강관",
|
||||
"source_line": 6406,
|
||||
"pum_form": "undetermined",
|
||||
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
|
||||
"pum_form": "requirement",
|
||||
"form_basis": "사람 판정 — 글값 — 값이 글로 적힌 소요량형(밑수는 표 밖이라 미확보로 남김)",
|
||||
"basis_quantity": 1.0,
|
||||
"basis_unit": "m",
|
||||
"basis_source": "본문",
|
||||
@@ -31129,8 +31129,8 @@
|
||||
"pum_table_id": "F0356",
|
||||
"section": "12-17-1. 펌프카 타설",
|
||||
"source_line": 6520,
|
||||
"pum_form": "undetermined",
|
||||
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
|
||||
"pum_form": "productivity",
|
||||
"form_basis": "사람 판정 — 혼합 — 값이 「㎥/일」이라 생산량형",
|
||||
"basis_quantity": null,
|
||||
"basis_unit": null,
|
||||
"basis_source": null,
|
||||
|
||||
@@ -21,7 +21,9 @@ sys.path.insert(0, str(ROOT))
|
||||
|
||||
|
||||
def newest_project() -> Path | None:
|
||||
candidates = [p for p in (ROOT / "storage").glob("*/*/*") if (p / "project_manifest.json").is_file()]
|
||||
candidates = [
|
||||
p for p in (ROOT / "storage").glob("*/*/*") if (p / "project_manifest.json").is_file()
|
||||
]
|
||||
return max(candidates, key=lambda p: p.stat().st_mtime) if candidates else None
|
||||
|
||||
|
||||
@@ -63,13 +65,21 @@ def main() -> int:
|
||||
marked = [s for s in stations if s.get("structure")]
|
||||
print(f"2) longitudinal.json 측점={len(stations)} 구조물 표식={len(marked)}건")
|
||||
for station in marked:
|
||||
print(f" - {station.get('chainage_m')}m {station.get('structure')} kind={station.get('kind')}")
|
||||
print(
|
||||
f" - {station.get('chainage_m')}m {station.get('structure')} kind={station.get('kind')}"
|
||||
)
|
||||
|
||||
# 3) 횡단 정본 파일
|
||||
cross_dir = project / "B06_Section" / "cross_sections"
|
||||
files = sorted(cross_dir.glob("cross_*.json")) if cross_dir.is_dir() else []
|
||||
with_structure = [(p.name, read_json(p).get("structure")) for p in files if (read_json(p) or {}).get("structure")]
|
||||
print(f"3) cross_*.json 파일={len(files)}개 구조물 표식={len(with_structure)}건 {with_structure}")
|
||||
with_structure = [
|
||||
(p.name, read_json(p).get("structure"))
|
||||
for p in files
|
||||
if (read_json(p) or {}).get("structure")
|
||||
]
|
||||
print(
|
||||
f"3) cross_*.json 파일={len(files)}개 구조물 표식={len(with_structure)}건 {with_structure}"
|
||||
)
|
||||
|
||||
# 4) B06 읽기 경로 — 실제로 세트가 붙는가
|
||||
from B06_Section.B06_Section_Engine_Culvert import load_culvert_sets
|
||||
|
||||
@@ -105,7 +105,8 @@ def main(argv):
|
||||
if not lengths:
|
||||
continue
|
||||
text = " ".join(
|
||||
f"{side}={'≥' if open_ else ''}{length:.2f}m" for side, (length, open_) in lengths.items()
|
||||
f"{side}={'≥' if open_ else ''}{length:.2f}m"
|
||||
for side, (length, open_) in lengths.items()
|
||||
)
|
||||
print(f"ch={chainage:8.1f} {section['design']['section_mode']:10s} {text}")
|
||||
|
||||
|
||||
@@ -59,4 +59,6 @@ for pipe_file in sorted(ROOT.rglob("pipe_points.json")):
|
||||
verdict = f"허용오차 안 (같은 노선) — {shift:.4f}m ≤ {ROUTE_MATCH_TOLERANCE_M}m"
|
||||
else:
|
||||
verdict = f"⚠ 투영 이월 가지 — {shift:.4f}m > {ROUTE_MATCH_TOLERANCE_M}m"
|
||||
print(f"{project_dir.name[:8]} 관 {len(points):3d}건 · 지문 {stored_sig[:18]:18s} · {src} · {verdict}")
|
||||
print(
|
||||
f"{project_dir.name[:8]} 관 {len(points):3d}건 · 지문 {stored_sig[:18]:18s} · {src} · {verdict}"
|
||||
)
|
||||
|
||||
@@ -24,7 +24,11 @@ ROOT = Path(STORAGE_BASE_DIR)
|
||||
def outer_trend(samples, side):
|
||||
"""바깥 5m 구간의 지반 기울기(수직/수평) — 양수면 바깥으로 갈수록 높아진다."""
|
||||
pts = sorted(
|
||||
((float(s["offset_m"]), float(s["elevation_m"])) for s in samples if s.get("valid") is not False),
|
||||
(
|
||||
(float(s["offset_m"]), float(s["elevation_m"]))
|
||||
for s in samples
|
||||
if s.get("valid") is not False
|
||||
),
|
||||
key=lambda p: p[0],
|
||||
)
|
||||
if side == "left":
|
||||
|
||||
@@ -67,7 +67,9 @@ async def main() -> None:
|
||||
pool = db.get_db_pool()
|
||||
rows = []
|
||||
async with pool.acquire() as conn, conn.cursor() as cur:
|
||||
await cur.execute("SELECT chainage_m, data FROM cross_sections WHERE route_id=%s", (ROUTE_ID,))
|
||||
await cur.execute(
|
||||
"SELECT chainage_m, data FROM cross_sections WHERE route_id=%s", (ROUTE_ID,)
|
||||
)
|
||||
for chainage, data in await cur.fetchall():
|
||||
doc = (json.loads(data) if isinstance(data, str) else data) or {}
|
||||
design = doc.get("design") or {}
|
||||
@@ -78,7 +80,9 @@ async def main() -> None:
|
||||
if not ground or len(line) < 2:
|
||||
continue
|
||||
need = need_width(
|
||||
line, ground, float(design.get("fill_slope_ratio") or 1.2),
|
||||
line,
|
||||
ground,
|
||||
float(design.get("fill_slope_ratio") or 1.2),
|
||||
float(design.get("cut_slope_ratio") or 1.0),
|
||||
)
|
||||
rows.append((float(chainage), need))
|
||||
@@ -90,8 +94,10 @@ async def main() -> None:
|
||||
print(f"· 아무리 넓혀도 안 닫히는 측·조합: {len(never)}건")
|
||||
if finite:
|
||||
finite.sort(key=lambda r: -r[2])
|
||||
print(f"· 넓히면 닫히는 조합: {len(finite)}건 — 필요한 추가 폭 최대 {finite[0][2]:.1f}m, "
|
||||
f"중앙값 {sorted(v for _, _, v in finite)[len(finite)//2]:.1f}m")
|
||||
print(
|
||||
f"· 넓히면 닫히는 조합: {len(finite)}건 — 필요한 추가 폭 최대 {finite[0][2]:.1f}m, "
|
||||
f"중앙값 {sorted(v for _, _, v in finite)[len(finite) // 2]:.1f}m"
|
||||
)
|
||||
for row in finite[:6]:
|
||||
print(f" {row[0]:8.1f}m {row[1]:5s} +{row[2]:.1f}m")
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""코리도 사전 생성 실경로 점검 — 실제 DB·실제 프로젝트로 한 번 돌려 본다(수동 실행)."""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""이관한 표가 예전 선·문자 그림과 같은 자리를 그리는지 확인 (수동 실행)."""
|
||||
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Table import (
|
||||
@@ -30,14 +32,17 @@ missing = [k for k in QUANTITY_VALUE_KEYS if k not in keyed and k not in ("cut",
|
||||
print("missing keys:", missing)
|
||||
|
||||
back = extract_quantity_table("draw1", {"entities": entities})
|
||||
same = all(
|
||||
back.get(k) == qt[k] for k in QUANTITY_VALUE_KEYS if k not in ("cut", "fill")
|
||||
)
|
||||
same = all(back.get(k) == qt[k] for k in QUANTITY_VALUE_KEYS if k not in ("cut", "fill"))
|
||||
print("round-trip values equal:", same)
|
||||
|
||||
box = _info_box_entities("draw1", 1, {"chainage_m": 100.0, "area_m2": 12345.0,
|
||||
"relief_m": 12.0, "flow_length_m": 34.0}, "#fff",
|
||||
(0.0, 0.0), 20.0)
|
||||
box = _info_box_entities(
|
||||
"draw1",
|
||||
1,
|
||||
{"chainage_m": 100.0, "area_m2": 12345.0, "relief_m": 12.0, "flow_length_m": 34.0},
|
||||
"#fff",
|
||||
(0.0, 0.0),
|
||||
20.0,
|
||||
)
|
||||
assert len(box) == 1 and box[0]["type"] == "Table"
|
||||
bs = box[0]["shapeData"]
|
||||
print("basin columns:", bs["columnWidths"], "rows:", len(bs["rowHeights"]))
|
||||
|
||||
@@ -71,7 +71,9 @@ def test_ditch_shapes_differ_between_types():
|
||||
"""엔진이 두 형식을 서로 다른 단면으로 낸다(3D는 이 단면을 잘라 쓴다)."""
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design
|
||||
|
||||
samples = [{"offset_m": o / 2, "elevation_m": 100.0 + o * 0.02, "valid": True} for o in range(-40, 41)]
|
||||
samples = [
|
||||
{"offset_m": o / 2, "elevation_m": 100.0 + o * 0.02, "valid": True} for o in range(-40, 41)
|
||||
]
|
||||
common = dict(
|
||||
samples=samples,
|
||||
design_elevation_m=100.0,
|
||||
|
||||
@@ -69,7 +69,10 @@ def test_widening_applies_to_outer_side_only() -> None:
|
||||
assert left["widening_left_m"] == 1.5 and left["widening_right_m"] == 0.0
|
||||
# 확폭은 붙은 쪽 차도 끝만 밖으로 민다.
|
||||
assert right["carriageway_edges"]["left"] == base["carriageway_edges"]["left"]
|
||||
assert right["carriageway_edges"]["right"]["offset_m"] < base["carriageway_edges"]["right"]["offset_m"]
|
||||
assert (
|
||||
right["carriageway_edges"]["right"]["offset_m"]
|
||||
< base["carriageway_edges"]["right"]["offset_m"]
|
||||
)
|
||||
assert right["carriageway_width_m"] == base["carriageway_width_m"] + 1.5
|
||||
|
||||
|
||||
@@ -94,9 +97,7 @@ def test_plan_radius_and_outer_side_from_polyline() -> None:
|
||||
angles = np.linspace(0.0, np.pi / 2, 400)
|
||||
radius = 50.0
|
||||
left_turn = np.column_stack([radius * np.cos(angles), radius * np.sin(angles)])
|
||||
chainage = np.r_[
|
||||
0.0, np.cumsum(np.hypot(np.diff(left_turn[:, 0]), np.diff(left_turn[:, 1])))
|
||||
]
|
||||
chainage = np.r_[0.0, np.cumsum(np.hypot(np.diff(left_turn[:, 0]), np.diff(left_turn[:, 1])))]
|
||||
radii, sides = _plan_radii(left_turn, chainage, np.array([40.0]), float(chainage[-1]))
|
||||
assert abs(radii[0] - radius) < 0.1
|
||||
assert sides[0] == "right"
|
||||
|
||||
@@ -34,7 +34,13 @@ _PANEL = _B06 / "B06_Section_UI_Standard_Panel.ts"
|
||||
|
||||
|
||||
def _sources() -> dict[str, str]:
|
||||
files = [_CONFIG, _CHROME, _PANEL, _B06 / "B06_Section_Schema.py", _B06 / "B06_Section_Router.py"]
|
||||
files = [
|
||||
_CONFIG,
|
||||
_CHROME,
|
||||
_PANEL,
|
||||
_B06 / "B06_Section_Schema.py",
|
||||
_B06 / "B06_Section_Router.py",
|
||||
]
|
||||
return {path.name: path.read_text(encoding="utf-8") for path in files}
|
||||
|
||||
|
||||
|
||||
@@ -101,7 +101,9 @@ def test_소유_측점에만_싣는다():
|
||||
layouts = _LAYOUTS.read_text(encoding="utf-8")
|
||||
assert "pipeOwnerChainage" in layouts, "소유 측점을 안 가리고 있다"
|
||||
assert "section.culvert?.chainage_m" in layouts, "관이 놓인 자리를 안 읽고 있다"
|
||||
py = (PROJECT_ROOT / "B06_Section" / "B06_Section_Engine_Culvert.py").read_text(encoding="utf-8")
|
||||
py = (PROJECT_ROOT / "B06_Section" / "B06_Section_Engine_Culvert.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
ts = (PROJECT_ROOT / "common_util" / "common_util_culvert_sets.ts").read_text(encoding="utf-8")
|
||||
assert '"chainage_m": pipe_chainage' in py
|
||||
assert "chainage_m: pipeChainage" in ts
|
||||
|
||||
@@ -120,7 +120,9 @@ def test_무대는_막힌_것이_아니다() -> None:
|
||||
|
||||
여기에 막힘 표시를 달면 「만들어야 할 것」 목록에 올라 없는 일이 생긴다.
|
||||
"""
|
||||
rows = 인계(haul_table={"rows": [{"equipment": "free_haul", "ground": "토사", "volume_m3": 10.0}]})
|
||||
rows = 인계(
|
||||
haul_table={"rows": [{"equipment": "free_haul", "ground": "토사", "volume_m3": 10.0}]}
|
||||
)
|
||||
row = next(r for r in rows if r["haul_equipment"] == "free_haul")
|
||||
assert row["in_bill"] is False
|
||||
assert row["blocked_kind"] is None
|
||||
|
||||
@@ -176,9 +176,24 @@ def test_무대는_내역줄이_아님() -> None:
|
||||
source = SummaryInput(
|
||||
earthwork_totals=토적표합계(),
|
||||
haul_rows=[
|
||||
{"equipment": "free_haul", "ground": "토사", "volume_m3": 871, "average_distance_m": 11.94},
|
||||
{"equipment": "dozer", "ground": "토사", "volume_m3": 1170, "average_distance_m": 43.66},
|
||||
{"equipment": "dump_truck", "ground": "암", "volume_m3": 1714, "average_distance_m": 318.6},
|
||||
{
|
||||
"equipment": "free_haul",
|
||||
"ground": "토사",
|
||||
"volume_m3": 871,
|
||||
"average_distance_m": 11.94,
|
||||
},
|
||||
{
|
||||
"equipment": "dozer",
|
||||
"ground": "토사",
|
||||
"volume_m3": 1170,
|
||||
"average_distance_m": 43.66,
|
||||
},
|
||||
{
|
||||
"equipment": "dump_truck",
|
||||
"ground": "암",
|
||||
"volume_m3": 1714,
|
||||
"average_distance_m": 318.6,
|
||||
},
|
||||
],
|
||||
)
|
||||
rows = build_rows(source)
|
||||
@@ -208,7 +223,14 @@ def test_평균운반거리가_비고에_남음() -> None:
|
||||
rows = build_rows(
|
||||
SummaryInput(
|
||||
earthwork_totals=토적표합계(),
|
||||
haul_rows=[{"equipment": "dozer", "ground": "토사", "volume_m3": 1170, "average_distance_m": 43.66}],
|
||||
haul_rows=[
|
||||
{
|
||||
"equipment": "dozer",
|
||||
"ground": "토사",
|
||||
"volume_m3": 1170,
|
||||
"average_distance_m": 43.66,
|
||||
}
|
||||
],
|
||||
)
|
||||
)
|
||||
dozer = next(r for r in rows if r.group == "도자운반")
|
||||
@@ -260,7 +282,11 @@ def test_깨진_파일이어도_화면은_서야_함(tmp_path: Path) -> None:
|
||||
|
||||
def test_한_구획만_갈아끼움(tmp_path: Path) -> None:
|
||||
"""두 페이지가 같은 파일을 쓴다 — 통째로 덮으면 상대 값이 사라진다."""
|
||||
save_section(tmp_path, "estimation", {"rate_dataset": {"dataset_id": "rates", "effective_date": "2026-04-13"}})
|
||||
save_section(
|
||||
tmp_path,
|
||||
"estimation",
|
||||
{"rate_dataset": {"dataset_id": "rates", "effective_date": "2026-04-13"}},
|
||||
)
|
||||
save_section(tmp_path, "quantity", {"rock_class_set": "uljin2"})
|
||||
stored = json.loads((tmp_path / "project_settings.json").read_text(encoding="utf-8"))
|
||||
# 남의 구획이 살아 있다 — B08 이 저장해도 B09 값이 안 지워진다.
|
||||
|
||||
@@ -103,7 +103,9 @@ def test_되돌릴_수_있어야_하는_숫자_칸은_계약에_등록될것() -
|
||||
}
|
||||
assert 숫자칸, "홑 숫자 칸을 하나도 못 골랐다 — 판정이 틀렸다"
|
||||
빠진것 = 숫자칸 - set(NULLABLE_SETTING_KEYS)
|
||||
assert not 빠진것, f"숫자 칸이 NULLABLE_SETTING_KEYS 에 없다 — 되돌릴 길이 없다: {sorted(빠진것)}"
|
||||
assert not 빠진것, (
|
||||
f"숫자 칸이 NULLABLE_SETTING_KEYS 에 없다 — 되돌릴 길이 없다: {sorted(빠진것)}"
|
||||
)
|
||||
|
||||
|
||||
def test_되돌리기_칸은_갈아끼우기_대상이기도_할것() -> None:
|
||||
|
||||
@@ -19,9 +19,9 @@ sys.path.insert(0, str(ROOT))
|
||||
from B08_Quantity.B08_Quantity_Engine_Pipe import build_rows # noqa: E402
|
||||
|
||||
MAPPING = json.loads(
|
||||
(
|
||||
ROOT / "resources" / "data_work_item_mapping" / "work_item_mapping_2026-01-01.json"
|
||||
).read_text(encoding="utf-8")
|
||||
(ROOT / "resources" / "data_work_item_mapping" / "work_item_mapping_2026-01-01.json").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)["pipe"]
|
||||
|
||||
|
||||
@@ -70,7 +70,9 @@ def test_연장이_오면_값이_선다() -> None:
|
||||
def test_관종으로_공종이_갈린다() -> None:
|
||||
보기 = {"파형강관": "FP-12-11-03", "흄관": "FP-12-11-02", "VR관": "FP-12-11-01"}
|
||||
for kind, code in 보기.items():
|
||||
out = build_rows([관(85.0, pipe_kind=kind, pipe_diameter_mm=800)], [길이(85.0, 9.0)], MAPPING)
|
||||
out = build_rows(
|
||||
[관(85.0, pipe_kind=kind, pipe_diameter_mm=800)], [길이(85.0, 9.0)], MAPPING
|
||||
)
|
||||
assert out["rows"][0]["work_item_code"] == code, kind
|
||||
|
||||
|
||||
|
||||
@@ -216,7 +216,12 @@ def test_법면보호공은_면고르기와_같은_값() -> None:
|
||||
def test_반영률_기본은_100퍼센트() -> None:
|
||||
"""실무 관측 80/50/80 은 참고일 뿐 기본값이 아니다 — 법대로 방침(8-10 ★)."""
|
||||
ratios = SlopeRatios()
|
||||
assert (ratios.bench_cut, ratios.face_dressing, ratios.slope_protection, ratios.tree_removal) == (
|
||||
assert (
|
||||
ratios.bench_cut,
|
||||
ratios.face_dressing,
|
||||
ratios.slope_protection,
|
||||
ratios.tree_removal,
|
||||
) == (
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
|
||||
@@ -591,7 +591,11 @@ def test_부속은_그_칸이_있을_때만_줄이_선다() -> None:
|
||||
def test_부속이_없는_종류에는_아무것도_안_붙는다() -> None:
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import attachments_of
|
||||
|
||||
옹벽 = {"structure_id": "s2", "type_id": "retaining_wall", "options": {"inlet_basin_form": "ㄷ형"}}
|
||||
옹벽 = {
|
||||
"structure_id": "s2",
|
||||
"type_id": "retaining_wall",
|
||||
"options": {"inlet_basin_form": "ㄷ형"},
|
||||
}
|
||||
assert attachments_of(옹벽) == []
|
||||
|
||||
|
||||
@@ -631,7 +635,10 @@ def test_서식_규칙이_정상_표를_지우지_말것() -> None:
|
||||
"""⚠ 「구분」·「단가」는 정상 표에도 흔하다 — 하나만 보면 정상 표가 사라진다."""
|
||||
from B08_Quantity.B08_Quantity_Build_WorkItemMaster import detect_form
|
||||
|
||||
소요량표 = {"headers": ["명칭", "규격", "단위", "수량"], "rows": [["보통인부", "", "인", "1.0"]]}
|
||||
소요량표 = {
|
||||
"headers": ["명칭", "규격", "단위", "수량"],
|
||||
"rows": [["보통인부", "", "인", "1.0"]],
|
||||
}
|
||||
assert detect_form(소요량표, "13")[0] == "requirement"
|
||||
구분표 = {
|
||||
"headers": [],
|
||||
|
||||
@@ -217,7 +217,8 @@ def test_사람이_가른_표_형태는_까닭을_달고_절_번호가_어긋나
|
||||
with pytest.raises(ValueError):
|
||||
judged_form("F0413", "13-4-5")
|
||||
assert all(why for _, _, why in FORM_JUDGMENTS.values())
|
||||
# 마스터에 실렸고, 파형강관 12-11-3 처럼 못 가른 표는 미판정으로 남는다
|
||||
# 마스터에 실렸고, 형태가 제대로 갈려 있다
|
||||
# (2026-09-17 파형강관 12-11-3 F0349 도 사람 판정으로 소요량형이 됨 — 산림 미판정 0)
|
||||
path = Path("resources/data_work_item_master/work_item_master_2026-01-01.json")
|
||||
tables = {
|
||||
t["pum_table_id"]: t
|
||||
@@ -226,7 +227,8 @@ def test_사람이_가른_표_형태는_까닭을_달고_절_번호가_어긋나
|
||||
}
|
||||
assert tables["F0413"]["pum_form"] == "reference"
|
||||
assert tables["F0189"]["pum_form"] == "productivity"
|
||||
assert tables["F0349"]["pum_form"] == "undetermined"
|
||||
assert tables["F0349"]["pum_form"] == "requirement"
|
||||
assert tables["F0349"]["form_basis"].startswith("사람 판정")
|
||||
|
||||
|
||||
def test_인력_찰쌓기는_표준경사_표에_막히지_않는다():
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
일할 표(작업량·소요량·형식 미정)를 가진 잎 공종인데 **제목도 사유도 없는** 것 — 표를 읽는 길이 없어
|
||||
한 줄도 안 서고 `unattached`·`component_gaps`·밑수·[주] 사유 어디에도 안 남아, 내역서엔 「일위대가가 아직
|
||||
없습니다」 만 뜸(무엇을 손볼지 모름). 예: 3-1 경계표시 F0075 「0.2 | 보통인부」(값이 이름 앞) ·
|
||||
6-4-1 덩굴걷기 F0158 · 7-3 아키야윈치 F0174 · 10-13-1 드론운반 F0322(형식 미정).
|
||||
6-4-1 덩굴걷기 F0158 · 7-3 아키야윈치 F0174.
|
||||
(10-13-1 드론운반 F0322 는 2026-09-17 사람 판정으로 풀림.)
|
||||
⇒ 읽는 길을 만드는 것은 뒤(예취기 한 모양 4 …) — 여기서는 **버린 표를 사유로 남김**(버릴 때는 사유).
|
||||
"""
|
||||
|
||||
@@ -47,7 +48,9 @@ def test_사유는_표_번호와_모양을_가리킴() -> None:
|
||||
("FP-03-08", "F0082"),
|
||||
):
|
||||
assert table in gaps[code] and "한 줄도" in gaps[code], (code, gaps.get(code))
|
||||
assert "F0322" in gaps["FP-10-13-01"] and "형식 미정" in gaps["FP-10-13-01"]
|
||||
# 2026-09-17 F0322 드론운반은 사람 판정으로 소요량형(묘목 1,000본당)이 됐다 —
|
||||
# 「형식 미정」 사유가 사라지고, 표를 읽는 길이 열렸다.
|
||||
assert "FP-10-13-01" not in gaps or "형식 미정" not in gaps["FP-10-13-01"]
|
||||
|
||||
|
||||
def test_부모와_제목_선_공종에는_안_붙음() -> None:
|
||||
|
||||
@@ -23,7 +23,7 @@ from common_util.common_util_crs import ( # noqa: E402
|
||||
)
|
||||
|
||||
UPLOADS = Path(r"C:/Users/umsan/.claude/uploads/ab448e1d-3fa5-4f88-8e02-b26428c8dfc7")
|
||||
LAS_PRJ = UPLOADS / "a450e281-__.prj" # Korean 1985 Modified East Belt → 5176
|
||||
LAS_PRJ = UPLOADS / "a450e281-__.prj" # Korean 1985 Modified East Belt → 5176
|
||||
ROUTE_PRJ = UPLOADS / "105c7734-______.__.2.2.prj" # UTM-K, AUTHORITY 없음 → 5179
|
||||
EXISTING_COMPD_PRJ = (
|
||||
ROOT / "storage/1/3/2f940d8a-2065-4cf6-8bf8-dc3f0af84e57/B03_FileInput/input/prj/result.prj"
|
||||
@@ -131,7 +131,22 @@ def test_analyze_existing_compound_prj_still_5187():
|
||||
|
||||
|
||||
# ── 광역 스윕: 한국 전체 + 해외, WKT 3형식, 익명화 악조건 ────────────────────
|
||||
KOREAN_CODES = (5173, 5174, 5175, 5176, 5177, 5178, 5179, 5185, 5186, 5187, 5188, 32651, 32652, 4326)
|
||||
KOREAN_CODES = (
|
||||
5173,
|
||||
5174,
|
||||
5175,
|
||||
5176,
|
||||
5177,
|
||||
5178,
|
||||
5179,
|
||||
5185,
|
||||
5186,
|
||||
5187,
|
||||
5188,
|
||||
32651,
|
||||
32652,
|
||||
4326,
|
||||
)
|
||||
FOREIGN_CODES = (3857, 32610, 25832, 2154, 27700, 26910, 2193, 6669, 6677, 3095, 28355, 31370)
|
||||
WKT_FORMATS = ("WKT1_GDAL", "WKT2_2019", "WKT1_ESRI")
|
||||
|
||||
|
||||
@@ -62,9 +62,7 @@ def test_도면은_잠금_도각_레이어로_나온다():
|
||||
assert {e["layerId"] for e in drawing["entities"]} == {FRAME_LAYER_ID}
|
||||
layers = {layer["id"]: layer for layer in drawing["layers"]}
|
||||
assert layers[FRAME_LAYER_ID]["isLocked"] is True
|
||||
assert any(not layer["isLocked"] for layer in drawing["layers"]), (
|
||||
"덧그릴 비잠금 도면층이 없다"
|
||||
)
|
||||
assert any(not layer["isLocked"] for layer in drawing["layers"]), "덧그릴 비잠금 도면층이 없다"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -19,9 +19,7 @@ sys.path.insert(0, str(ROOT))
|
||||
from common_util.common_util_crs import crs_input_from_prj, identify_epsg # noqa: E402
|
||||
|
||||
COMPOUND_PRJ = next(ROOT.glob("storage/*/*/*/B03_FileInput/input/prj/result.prj"), None)
|
||||
ESRI_PRJ = next(
|
||||
ROOT.glob("resources/knowledge/original/실무문서/**/*중심선(EPSG5176).prj"), None
|
||||
)
|
||||
ESRI_PRJ = next(ROOT.glob("resources/knowledge/original/실무문서/**/*중심선(EPSG5176).prj"), None)
|
||||
|
||||
|
||||
def _read(path: Path) -> str:
|
||||
|
||||
@@ -26,8 +26,12 @@ def _project_root_and_surface():
|
||||
|
||||
async def run():
|
||||
conn = await aiomysql.connect(
|
||||
host=DB_HOST, port=DB_PORT, user=DB_USER, password=DB_PASSWORD,
|
||||
db=DB_NAME, charset="utf8mb4",
|
||||
host=DB_HOST,
|
||||
port=DB_PORT,
|
||||
user=DB_USER,
|
||||
password=DB_PASSWORD,
|
||||
db=DB_NAME,
|
||||
charset="utf8mb4",
|
||||
)
|
||||
try:
|
||||
async with conn.cursor(aiomysql.DictCursor) as cursor:
|
||||
@@ -35,7 +39,9 @@ def _project_root_and_surface():
|
||||
"SELECT storage_path FROM projects WHERE id = %s", (PROJECT_ID,)
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
from common_util.common_util_surface_confirmation import get_surface_confirmation_params
|
||||
from common_util.common_util_surface_confirmation import (
|
||||
get_surface_confirmation_params,
|
||||
)
|
||||
|
||||
params = await get_surface_confirmation_params(conn, PROJECT_ID)
|
||||
return row, params
|
||||
@@ -114,7 +120,9 @@ def test_edge_trim_3m_ends_are_not_spiky():
|
||||
assert len(trimmed_3) >= 2
|
||||
head, tail, total_3 = _head_tail_within(trimmed_3, 20.0)
|
||||
_, _, total_30 = _head_tail_within(trimmed_30, 20.0)
|
||||
print(f"\n연장: 30m 트림 {total_30:.1f}m / 3m 트림 {total_3:.1f}m (차이 {total_3 - total_30:.1f}m)")
|
||||
print(
|
||||
f"\n연장: 30m 트림 {total_30:.1f}m / 3m 트림 {total_3:.1f}m (차이 {total_3 - total_30:.1f}m)"
|
||||
)
|
||||
|
||||
inner = _slopes(sampler, trimmed_3[len(trimmed_3) // 4 : 3 * len(trimmed_3) // 4])
|
||||
ends = _slopes(sampler, head) + _slopes(sampler, tail)
|
||||
|
||||
@@ -21,8 +21,12 @@ def _project_root() -> Path:
|
||||
|
||||
async def run():
|
||||
conn = await aiomysql.connect(
|
||||
host=DB_HOST, port=DB_PORT, user=DB_USER, password=DB_PASSWORD,
|
||||
db=DB_NAME, charset="utf8mb4",
|
||||
host=DB_HOST,
|
||||
port=DB_PORT,
|
||||
user=DB_USER,
|
||||
password=DB_PASSWORD,
|
||||
db=DB_NAME,
|
||||
charset="utf8mb4",
|
||||
)
|
||||
try:
|
||||
async with conn.cursor(aiomysql.DictCursor) as cursor:
|
||||
@@ -50,7 +54,9 @@ def test_las_preview_points():
|
||||
meta = analyze_las_metadata(files[0])
|
||||
elapsed = time.perf_counter() - started
|
||||
points = meta.get("preview_points") or []
|
||||
print(f"\n{files[0].name}: 점 {meta['point_count']:,}개, 미리보기 점 {len(points)}개, {elapsed:.1f}s")
|
||||
print(
|
||||
f"\n{files[0].name}: 점 {meta['point_count']:,}개, 미리보기 점 {len(points)}개, {elapsed:.1f}s"
|
||||
)
|
||||
assert 0 < len(points) <= 5000
|
||||
bounds = meta["bounds"]
|
||||
for x, y in points[:200]:
|
||||
@@ -69,7 +75,9 @@ def test_geotiff_thumbnail():
|
||||
meta = analyze_tif_metadata(files[0])
|
||||
elapsed = time.perf_counter() - started
|
||||
thumb = meta.get("preview_thumbnail")
|
||||
print(f"{files[0].name}: 썸네일 {'있음 %d바이트' % len(thumb) if thumb else '없음(오버뷰 없음)'}, {elapsed:.1f}s")
|
||||
print(
|
||||
f"{files[0].name}: 썸네일 {'있음 %d바이트' % len(thumb) if thumb else '없음(오버뷰 없음)'}, {elapsed:.1f}s"
|
||||
)
|
||||
if thumb:
|
||||
assert thumb.startswith("data:image/png;base64,")
|
||||
assert elapsed < 5.0
|
||||
|
||||
@@ -46,7 +46,9 @@ def test_collect_cost_is_small():
|
||||
meta = analyze_las_metadata(path)
|
||||
current = time.perf_counter() - started
|
||||
|
||||
print(f"\n분류 통계만 {baseline:.2f}s / 점 수집 포함 {current:.2f}s "
|
||||
f"(차이 {current - baseline:+.2f}s, 점 {len(meta['preview_points'])}개)")
|
||||
print(
|
||||
f"\n분류 통계만 {baseline:.2f}s / 점 수집 포함 {current:.2f}s "
|
||||
f"(차이 {current - baseline:+.2f}s, 점 {len(meta['preview_points'])}개)"
|
||||
)
|
||||
# 파일을 다시 읽지 않으므로 한 번 훑는 시간과 크게 다르지 않아야 한다.
|
||||
assert current <= baseline * 1.5 + 1.0
|
||||
|
||||
@@ -14,10 +14,18 @@ from pyproj import CRS
|
||||
|
||||
# 한국에서 실제로 들어오는 좌표계만 후보로 둔다.
|
||||
CANDIDATES = (
|
||||
5185, 5186, 5187, 5188, # Korea 2000 (GRS80) 서·중부·동부·동해
|
||||
5179, 5178, # UTM-K (GRS80 / Bessel)
|
||||
5173, 5174, 5176, 5177, # Korean 1985 (Bessel) 구 좌표계
|
||||
32652, 4326,
|
||||
5185,
|
||||
5186,
|
||||
5187,
|
||||
5188, # Korea 2000 (GRS80) 서·중부·동부·동해
|
||||
5179,
|
||||
5178, # UTM-K (GRS80 / Bessel)
|
||||
5173,
|
||||
5174,
|
||||
5176,
|
||||
5177, # Korean 1985 (Bessel) 구 좌표계
|
||||
32652,
|
||||
4326,
|
||||
)
|
||||
_AUTHORITY = re.compile(r'AUTHORITY\["EPSG","(\d+)"\]')
|
||||
|
||||
@@ -84,7 +92,7 @@ def test_uploaded_prj_files() -> None:
|
||||
route = open(upload + "/105c7734-______.__.2.2.prj", encoding="utf-8").read()
|
||||
except FileNotFoundError as e:
|
||||
pytest.skip(f"laptop 업로드 픽스처 없음 (환경 의존): {e.filename}")
|
||||
assert epsg_from_prj(las) == 5176, epsg_from_prj(las) # AUTHORITY 태그로 판별
|
||||
assert epsg_from_prj(las) == 5176, epsg_from_prj(las) # AUTHORITY 태그로 판별
|
||||
assert epsg_from_prj(route) == 5179, epsg_from_prj(route) # 파라미터 지문으로 판별
|
||||
|
||||
|
||||
|
||||
@@ -21,8 +21,12 @@ def _project_root() -> Path:
|
||||
|
||||
async def run():
|
||||
conn = await aiomysql.connect(
|
||||
host=DB_HOST, port=DB_PORT, user=DB_USER, password=DB_PASSWORD,
|
||||
db=DB_NAME, charset="utf8mb4",
|
||||
host=DB_HOST,
|
||||
port=DB_PORT,
|
||||
user=DB_USER,
|
||||
password=DB_PASSWORD,
|
||||
db=DB_NAME,
|
||||
charset="utf8mb4",
|
||||
)
|
||||
try:
|
||||
async with conn.cursor(aiomysql.DictCursor) as cursor:
|
||||
|
||||
@@ -42,15 +42,17 @@ MASTER = OUT_DIR / "work_item_master_2026-01-01.json"
|
||||
|
||||
#: 열쇠를 얹기 **전** 벌(`work_items`)의 지문. 2026-09-17 데스크탑_서브가 뜬 값.
|
||||
#: ⚠ 이 값이 흔들리면 값이 움직였다는 뜻이다 — 지문을 고치지 말고 **원인을 먼저 찾을 것.**
|
||||
#: 2026-09-17 한 번 갱신 — 표머리 몫꼴(`인/ha`·`인/100kg`)에서 **밑수 18개를 새로 읽음.**
|
||||
#: ⚠ **덮어쓴 것 0 · 사라진 것 0**(더하기만) — 곱하면 안 되던 줄이 곱할 수 있게 된 것뿐이다.
|
||||
#: 2026-09-17 두 번 갱신 — 둘 다 **더하기만**(덮어쓴 것 0 · 사라진 것 0):
|
||||
#: ① 표머리 몫꼴(`인/ha`·`인/100kg`)에서 밑수 18개를 새로 읽음
|
||||
#: ② 형태 미판정 14표를 사람 판정으로 가름(안티그래비티 원문 대조) · 그중 5표는 밑수도 확인
|
||||
#: ⇒ 곱하면 안 되던 줄이 곱할 수 있게 된 것이지, 있던 값이 바뀐 것이 아니다.
|
||||
#: 2026-09-17 랩탑 메인 — 목차 오기 둘을 본문 번호로 바로잡아(브레인 지시) **네 줄만** 바뀌어 새로 뜸
|
||||
#: (옛 f8b8fba1…). 네 줄 모습은 `test_목차_오기는_본문_번호로_바로잡고_열쇠는_그대로` 가 잼 ·
|
||||
#: 미판정·밑수 목록 파일은 한 글자도 안 바뀜.
|
||||
#: 2026-09-17 랩탑 메인 둘째 — 헛단위 뺌(인용을 제목으로 읽은 13-3 표 셋 · 부록 사례 번호 겹침 일곱)으로 다시 뜸
|
||||
#: (옛 be88f730…). 바뀐 줄은 13-3·4-1·4-2·2-1·2-2·3-1·3-2·3-3·13-4-1·13-5-2 의 표 귀속뿐 —
|
||||
#: `test_work_item_master_toc` 가 자리를 잼 · 단가표 대조는 그 파일 머리.
|
||||
BASELINE_WORK_ITEMS_SHA = "40746aa3b4a2ac6a78323590069242088f1fb44f4662dc2f240afe303f385978"
|
||||
BASELINE_WORK_ITEMS_SHA = "cd9a0e755249fb408c01499e980afed02a7b882b36ae7029a5bd35ab63f5c5da"
|
||||
|
||||
#: 열쇠 작업이 새로 얹은 칸. 지문을 잴 때만 떼어낸다.
|
||||
ADDED_FIELDS = (
|
||||
@@ -329,6 +331,36 @@ def test_갈래_문자열이_열쇠라_흔들림을_못박음(master: dict) -> N
|
||||
assert len(messy) == 6, f"공백·물결표만 다른 짝이 {len(messy)} — 6 이던 것"
|
||||
|
||||
|
||||
def test_산림_형태_미판정이_없다(master: dict) -> None:
|
||||
"""2026-09-17 안티그래비티 원문 대조로 마지막 14표를 가름 — 미판정이 0 이 됐다.
|
||||
|
||||
⚠ 형태가 뒤집히면 값이 20배쯤 조용히 틀린다. 미판정으로 남겨 두는 편이 안전했으나,
|
||||
14표 모두 **금액 대상**이라 비워 두면 그 공종이 통째로 안 선다.
|
||||
"""
|
||||
from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Forms import BASIS_JUDGMENTS, judged_form
|
||||
|
||||
forms = [t["pum_form"] for it in master["work_items"] for t in it["tables"]]
|
||||
assert "undetermined" not in forms
|
||||
assert master["stats"]["form_undetermined"] == 0
|
||||
# 열넷이 실제로 사람 판정으로 섰는가
|
||||
가른것 = {
|
||||
"F0140": "requirement", "F0141": "requirement", "F0142": "requirement",
|
||||
"F0195": "productivity", "F0248": "coefficient", "F0322": "requirement",
|
||||
"F0323": "requirement", "F0333": "requirement", "F0346": "requirement",
|
||||
"F0169": "productivity", "F0304": "requirement", "F0356": "productivity",
|
||||
"F0257": "coefficient", "F0349": "requirement",
|
||||
} # fmt: skip
|
||||
by_id = {t["pum_table_id"]: t for it in master["work_items"] for t in it["tables"]}
|
||||
for table_id, form in 가른것.items():
|
||||
assert by_id[table_id]["pum_form"] == form, table_id
|
||||
assert by_id[table_id]["form_basis"].startswith("사람 판정"), table_id
|
||||
# 머리가 소실돼 자동으로는 못 읽는 표의 밑수만 손으로 적는다
|
||||
assert set(BASIS_JUDGMENTS) <= set(가른것)
|
||||
assert by_id["F0140"]["basis_quantity"] == 100.0 and by_id["F0140"]["basis_unit"] == "㎡"
|
||||
assert by_id["F0322"]["basis_quantity"] == 1000.0 and by_id["F0322"]["basis_unit"] == "본"
|
||||
assert judged_form("F0140", "5-26-1")[0] == "requirement"
|
||||
|
||||
|
||||
def test_표머리_몫꼴_밑수_읽기() -> None:
|
||||
"""「당」 없이 몫으로만 적힌 표머리에서 밑수를 읽는다 — 분모가 밑수다(명세 꼴 B).
|
||||
|
||||
|
||||
@@ -84,8 +84,8 @@ def test_트리_표_id_와_이름표_표_id_는_양쪽으로_같음(client: Test
|
||||
label_only += [f"{entry['file_id']}::{i}" for i in sorted(label_ids - tree_ids)]
|
||||
assert not_labelled == [], not_labelled
|
||||
assert label_only == [], label_only
|
||||
# 2026-09-17 92 → 101 — 공종 축 산출 일곱 + 갈래 열쇠 장부 표.
|
||||
assert sum(len(t) for t in files.values()) == LABELS["counts"]["tables"] == 101
|
||||
# 2026-09-17 92 → 100 — 공종 축 산출 일곱 + 갈래 열쇠 장부 표, 형태 미판정 표는 비어 빠짐.
|
||||
assert sum(len(t) for t in files.values()) == LABELS["counts"]["tables"] == 100
|
||||
for entry in LABELS["files"]: # 이름이 실제로 붙음 · 줄 수도 이름표 셈과 같음
|
||||
named = {t["key"]: t for t in entry["tables"]}
|
||||
for t in files[entry["file_id"]]:
|
||||
|
||||
@@ -38,7 +38,9 @@ def _get(client: TestClient, kind: str, **params) -> dict:
|
||||
def test_산림_평평한_잎_목록과_계산_규칙(client: TestClient) -> None:
|
||||
listed = client.get("/api/master-data/base-prices").json()["kinds"]
|
||||
forest = next(i for i in listed if i["kind"] == "work_item_forest")
|
||||
assert forest["group"] == "work_item" and forest["rows"] == 620 # 갈래마다 한 줄 · 헛단위 7줄 뺌(2026-09-17)
|
||||
assert (
|
||||
forest["group"] == "work_item" and forest["rows"] == 620
|
||||
) # 갈래마다 한 줄 · 헛단위 7줄 뺌(2026-09-17)
|
||||
# 상자 이름은 이름표 것 — 영문 키(work_item)로 뜨던 자리(2026-09-17 브레인 ①)
|
||||
assert {i["group"]: i["group_label"] for i in listed} == {
|
||||
"base_price": "기초단가",
|
||||
|
||||
Reference in New Issue
Block a user