main_laptop_1 -> main byeonghap (4 hwangyeong 585 commits) #12
@@ -60,6 +60,38 @@ MASONRY_DIR = Path(__file__).resolve().parents[1] / "resources" / "data_masonry"
|
||||
MASONRY_PREFIX = "masonry_class_"
|
||||
|
||||
|
||||
#: 목재공작물 구조 갈래표 — 품셈 13-13-1 [주]③ 이 **재료 구성**으로 가른다.
|
||||
TIMBER_DIR = Path(__file__).resolve().parents[1] / "resources" / "data_timber"
|
||||
TIMBER_PREFIX = "timber_structure_class_"
|
||||
|
||||
|
||||
def load_timber_table(path: Path | None = None) -> dict[str, Any]:
|
||||
"""목재공작물 갈래표. 파일이 없으면 빈 표 — 갈래가 안 붙고 그대로 드러난다."""
|
||||
target = path
|
||||
if target is None:
|
||||
files = sorted(TIMBER_DIR.glob(TIMBER_PREFIX + "*.json")) if TIMBER_DIR.is_dir() else []
|
||||
target = files[-1] if files else None
|
||||
if target is None or not target.is_file():
|
||||
return {}
|
||||
return json.loads(target.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def timber_class(type_id: str, table: dict[str, Any] | None = None) -> tuple[str | None, str, bool]:
|
||||
"""(갈래, 근거, 잠정인가). **잠정이면 그 사실을 숨기지 않는다.**
|
||||
|
||||
⚠ 갈래를 고르되 **드러낸다** — 임의로 고르고 조용히 넘어가면 미결을 숨기는 것이다.
|
||||
밑수 1㎥ 는 **목재 채적**이라 「1㎥에 건축목공 17인」이 말이 된다(원문 [주]③).
|
||||
"""
|
||||
found = table if table is not None else load_timber_table()
|
||||
for row in (found or {}).get("type_map") or []:
|
||||
if row.get("type_id") == type_id and row.get("class"):
|
||||
basis = f"품셈 13-13-1 [주]③ 「{row.get('matched')}」"
|
||||
if row.get("provisional"):
|
||||
basis += f" · ⚠ 잠정 — {row.get('compare', '')}"
|
||||
return str(row["class"]), basis, bool(row.get("provisional"))
|
||||
return None, f"품셈 13-13-1 [주]③ 예시에 없는 공작물({type_id}) — 임의로 고르지 않음", False
|
||||
|
||||
|
||||
def load_masonry_table(path: Path | None = None) -> dict[str, Any]:
|
||||
"""돌쌓기 갈래표. 파일이 없으면 빈 표 — 갈래가 안 붙고 그대로 드러난다."""
|
||||
target = path
|
||||
|
||||
@@ -34,13 +34,59 @@ EROSION_CONTROL_TYPES = frozenset(
|
||||
}
|
||||
)
|
||||
|
||||
#: 규준틀 — **품셈 원문이 개소 기준을 정해 둔다**(2026-09-07 ㉒ 에서 찾음).
|
||||
#: 11-2 [주]① 「비탈길이 **10m 이상** **20m마다** 설치한다」
|
||||
#: 11-3 [주]① 「중심점에서 **성토 높이 5m 이상**에 설치한다」
|
||||
#: ⚠ 재료량은 [주]④ 「설계수량에 따른다」 — **개소만 내고 재료는 미확보**로 둔다.
|
||||
BATTER_MIN_SLOPE_LENGTH_M = 10.0
|
||||
BATTER_INTERVAL_M = 20.0
|
||||
LEVEL_MIN_FILL_HEIGHT_M = 5.0
|
||||
|
||||
STATUS_COUNTED_ELSEWHERE = "다른 표에서 이미 섬"
|
||||
STATUS_PENDING = "값을 낼 근거가 없음"
|
||||
STATUS_NOT_APPLICABLE = "해당 없음"
|
||||
STATUS_READY = "값 있음"
|
||||
|
||||
|
||||
def preparation_rows(slope_totals: dict[str, float] | None = None) -> list[dict[str, Any]]:
|
||||
def batter_frame_count(slope_rows: Iterable[dict[str, Any]]) -> tuple[int, list[str]]:
|
||||
"""비탈 규준틀 개소 — **비탈길이 10m 이상인 구간에서 20m마다**(품셈 11-2 [주]①).
|
||||
|
||||
⚠ 「10m 이상」은 **비탈길이**(사면길이) 조건이고 「20m마다」는 **노선 거리** 간격이다.
|
||||
둘을 섞지 않는다 — 사면이 긴 구간의 **연장**을 20m 로 나눈다.
|
||||
"""
|
||||
length_m = 0.0
|
||||
notes: list[str] = []
|
||||
for row in slope_rows:
|
||||
lengths = row.get("lengths") or {}
|
||||
# 그 측점의 사면길이는 계열마다 있으나 **가장 긴 것**으로 본다(같은 사면이다).
|
||||
longest = max((float(v) for v in lengths.values()), default=0.0)
|
||||
if longest >= BATTER_MIN_SLOPE_LENGTH_M:
|
||||
length_m += float(row.get("distance_m") or 0.0)
|
||||
if length_m <= 0:
|
||||
return 0, ["비탈길이 10m 이상인 구간이 없어 비탈 규준틀이 서지 않음"]
|
||||
count = int(length_m // BATTER_INTERVAL_M) + 1
|
||||
notes.append(
|
||||
f"비탈길이 {BATTER_MIN_SLOPE_LENGTH_M:g}m 이상 구간 {length_m:g}m ÷ "
|
||||
f"{BATTER_INTERVAL_M:g}m + 1 (품셈 11-2 [주]①)"
|
||||
)
|
||||
return count, notes
|
||||
|
||||
|
||||
def level_frame_count(slope_rows: Iterable[dict[str, Any]]) -> tuple[int, list[str]]:
|
||||
"""수평 규준틀 — **중심점 성토 높이 5m 이상**에 설치(품셈 11-3 [주]①).
|
||||
|
||||
⚠ 성토고는 사면표에 없다 — 그 값을 받기 전에는 **0 으로 때우지 않고 미확보**로 둔다.
|
||||
"""
|
||||
return 0, [
|
||||
"중심점 성토고 5m 이상 지점에 설치(품셈 11-3 [주]①)인데 성토고가 이 표에 없어 개소를 못 셈"
|
||||
]
|
||||
|
||||
|
||||
def preparation_rows(
|
||||
slope_totals: dict[str, float] | None = None,
|
||||
slope_rows: Iterable[dict[str, Any]] = (),
|
||||
topsoil_thickness_m: float | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""준비공 줄 — 값이 서는 것과 안 서는 것을 **한 목록에** 낸다."""
|
||||
slope = slope_totals or {}
|
||||
tree_area = float(slope.get("tree_removal_fill", 0.0)) + float(
|
||||
@@ -62,15 +108,7 @@ def preparation_rows(slope_totals: dict[str, float] | None = None) -> list[dict[
|
||||
),
|
||||
"work_item_code": None,
|
||||
},
|
||||
{
|
||||
"group": "준비공",
|
||||
"item": "표토제거",
|
||||
"unit": "㎥",
|
||||
"amount": None,
|
||||
"status": STATUS_PENDING,
|
||||
"reason": "면적은 사면적에서 나오나 **두께·대상 구간**이 설계로 안 정해져 있음 (품셈 9-15)",
|
||||
"work_item_code": "FP-09-15",
|
||||
},
|
||||
_topsoil_row(slope, topsoil_thickness_m),
|
||||
{
|
||||
"group": "준비공",
|
||||
"item": "제근·뿌리다듬기",
|
||||
@@ -80,18 +118,67 @@ def preparation_rows(slope_totals: dict[str, float] | None = None) -> list[dict[
|
||||
"reason": "단위가 「개」(그루 수)인데 입목 본수를 들고 있지 않음 (품셈 9-20~21)",
|
||||
"work_item_code": "FP-09-21",
|
||||
},
|
||||
_batter_frame_row(list(slope_rows)),
|
||||
{
|
||||
"group": "준비공",
|
||||
"item": "규준틀",
|
||||
"item": "수평 규준틀",
|
||||
"unit": "개소",
|
||||
"amount": None,
|
||||
"status": STATUS_PENDING,
|
||||
"reason": "개소 산정 기준(구조물·절성토 구간별 몇 개소)이 안 정해져 있음 (품셈 11-2)",
|
||||
"work_item_code": "FP-11-02",
|
||||
"reason": level_frame_count(())[1][0],
|
||||
"work_item_code": "FP-11-03",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _topsoil_row(slope: dict[str, float], thickness_m: float | None) -> dict[str, Any]:
|
||||
"""표토제거 — **두께는 품셈이 아니라 설계가 정한다**(9-15 [주]② 「T : 표토두께(m)」).
|
||||
|
||||
⚠ 두께를 안 넣으면 **0 으로 때우지 않고** 물량을 안 낸다. 대상 면적은 절·성토 사면적을
|
||||
쓴다(사면 계열의 면고르기 면적과 같은 자리).
|
||||
"""
|
||||
area = float(slope.get("face_dressing_fill", 0.0)) + float(slope.get("face_dressing_cut", 0.0))
|
||||
if thickness_m is None or float(thickness_m) <= 0:
|
||||
return {
|
||||
"group": "준비공",
|
||||
"item": "표토제거",
|
||||
"unit": "㎥",
|
||||
"amount": None,
|
||||
"status": STATUS_PENDING,
|
||||
"reason": (
|
||||
"표토 두께가 아직 입력되지 않았습니다 — 품셈 9-15 [주]② 가 두께를 "
|
||||
"「공식의 입력 변수(T)」로 두어 **품셈이 정하는 값이 아닙니다**. "
|
||||
f"산출 조건에서 두께를 넣으면 값이 섭니다 (대상 면적 {area:,.1f}㎡)"
|
||||
),
|
||||
"reference_amount": area,
|
||||
"work_item_code": "FP-09-15",
|
||||
}
|
||||
thickness = float(thickness_m)
|
||||
return {
|
||||
"group": "준비공",
|
||||
"item": "표토제거",
|
||||
"unit": "㎥",
|
||||
"amount": area * thickness,
|
||||
"status": STATUS_READY,
|
||||
"reason": f"사면적 {area:,.1f}㎡ × 두께 {thickness:g}m (품셈 9-15)",
|
||||
"work_item_code": "FP-09-15",
|
||||
}
|
||||
|
||||
|
||||
def _batter_frame_row(slope_rows: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
"""비탈 규준틀 한 줄. **개소는 원문 기준으로 서고 재료는 미확보**다."""
|
||||
count, notes = batter_frame_count(slope_rows)
|
||||
return {
|
||||
"group": "준비공",
|
||||
"item": "비탈 규준틀",
|
||||
"unit": "개소",
|
||||
"amount": float(count) if count else None,
|
||||
"status": STATUS_READY if count else STATUS_PENDING,
|
||||
"reason": ("; ".join(notes) + " · 재료량은 품셈 11-2 [주]④ 「설계수량에 따른다」라 미확보"),
|
||||
"work_item_code": "FP-11-02",
|
||||
}
|
||||
|
||||
|
||||
def erosion_rows(structures: Iterable[dict[str, Any]] = ()) -> list[dict[str, Any]]:
|
||||
"""사방공 줄 — 이 노선에 사방 시설이 **있을 때만** 값이 선다."""
|
||||
found = sorted(
|
||||
@@ -130,9 +217,13 @@ def erosion_rows(structures: Iterable[dict[str, Any]] = ()) -> list[dict[str, An
|
||||
def build_table(
|
||||
slope_totals: dict[str, float] | None = None,
|
||||
structures: Iterable[dict[str, Any]] = (),
|
||||
slope_rows: Iterable[dict[str, Any]] = (),
|
||||
topsoil_thickness_m: float | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""화면·인계가 그대로 쓰는 모양. **못 서는 줄도 목록에 남는다.**"""
|
||||
rows = preparation_rows(slope_totals) + erosion_rows(structures)
|
||||
rows = preparation_rows(slope_totals, slope_rows, topsoil_thickness_m) + erosion_rows(
|
||||
structures
|
||||
)
|
||||
return {
|
||||
"columns": ["구분", "공종", "단위", "수량", "상태", "사유"],
|
||||
"rows": rows,
|
||||
|
||||
@@ -100,7 +100,10 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse:
|
||||
)
|
||||
# 준비공·사방공 — 못 서는 줄도 사유와 함께 남긴다(빈 표는 「빠뜨림」과 구별이 안 됨).
|
||||
table["preparation"] = build_preparation_table(
|
||||
slope.get("totals") or {}, await _route_structures(project_id)
|
||||
slope.get("totals") or {},
|
||||
await _route_structures(project_id),
|
||||
slope.get("rows") or [],
|
||||
settings.get("topsoil_thickness_m"),
|
||||
)
|
||||
method, method_is_default = concrete_placing_method(settings)
|
||||
# ⚠ 금액에 바로 걸리는 값이라 「기본값으로 돌고 있음」을 응답에 실어 화면이 띄우게 한다.
|
||||
@@ -179,6 +182,8 @@ class QuantitySettingsBody(BaseModel):
|
||||
material_supply: dict[str, Any] | None = None
|
||||
# 콘크리트 타설 방식. `""` 는 「안 정함」으로 되돌리는 뜻이라 서버가 None 으로 만든다.
|
||||
concrete_placing_method: str | None = None
|
||||
# 표토제거 두께(m). 품셈이 정하는 값이 아니라 설계 입력이다(9-15 [주]②).
|
||||
topsoil_thickness_m: float | None = None
|
||||
|
||||
|
||||
@router.put("/{project_id}/quantity/settings")
|
||||
|
||||
@@ -92,6 +92,10 @@ def default_settings() -> dict[str, Any]:
|
||||
# 또는 `{자재명: {"supply": …, "install_by": "contractor"|"owner"}}`.
|
||||
# ⚠ **법이 아니라 발주 결정**이라 기본은 비워 둔다. 안 정한 자재는 「미분류」로
|
||||
# 화면에 드러난다 — 사급으로 조용히 넘기면 관급자재대가 새 나간다.
|
||||
# 표토제거 두께(m) — ⚠ **품셈이 정하는 값이 아니다.** 9-15 [주]② 가
|
||||
# 「T : 표토두께(m)」로 **공식의 입력 변수**로 두었다(2026-09-07 원문 확인).
|
||||
# 기본값을 두지 않는다 — 안 넣으면 물량을 안 낸다(0 으로 때우지 않음).
|
||||
"topsoil_thickness_m": None,
|
||||
"material_supply": {},
|
||||
# 콘크리트 타설 방식 — `ready_mixed`(FP-12-01-01) / `machine_mixed`(-02) /
|
||||
# `hand_mixed`(-03). **설계 판단**이라 사용자가 고른다.
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"dataset_id": "timber_structure_class",
|
||||
"effective_date": "2026-01-01",
|
||||
"note": "목재공작물 구조 갈래(품셈 13-13-1 [주]③). **재료 구성**으로 가르며 원문이 예시를 든다. 구조물 종류가 늘면 `type_map` 에 줄만 더한다.",
|
||||
"source": {
|
||||
"doc": "산림사업 표준품셈 13-13-1 목재틀흙막이 [주]③",
|
||||
"table_id": "F0440",
|
||||
"quote": "보통구조 : 통나무나 대각재, 후판 등이 대부분(80%) 이상으로, 목재 채적에 비해 가공정도가 적은 공작물 (하: 통나무 경계목 / 중: 통나무 방풍책 / 상: 통나무 기슭막이) · 중등구조 : 통나무나 대각재, 후판 등이 절반(50%) 이상 (보통: 통나무 바닥막이·누구막이 / 상: 통나무 골막이) · 상등구조 : 소각재, 박판, 소폭판 등이 목재의 50% 이상 (통나무 사방댐 등)"
|
||||
},
|
||||
"basis_unit": {
|
||||
"note": "⚠ 밑수 1㎥ 는 **구조물 체적이 아니라 목재 채적**이다. 원문이 「목재 채적에 비해 가공정도」로 갈래를 가르는 데서 드러난다. 그래야 「1㎥에 건축목공 17인」이 말이 된다.",
|
||||
"unit": "목재 채적 ㎥"
|
||||
},
|
||||
"classes": [
|
||||
{
|
||||
"key": "보통구조 하",
|
||||
"carpenter": 6.285,
|
||||
"laborer": 0.682,
|
||||
"examples": [
|
||||
"통나무 경계목"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "보통구조 중",
|
||||
"carpenter": 7.274,
|
||||
"laborer": 0.786,
|
||||
"examples": [
|
||||
"통나무 방풍책"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "보통구조 상",
|
||||
"carpenter": 8.76,
|
||||
"laborer": 0.958,
|
||||
"examples": [
|
||||
"통나무 기슭막이"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "중등구조 보통",
|
||||
"carpenter": 10.612,
|
||||
"laborer": 1.156,
|
||||
"examples": [
|
||||
"통나무 바닥막이",
|
||||
"누구막이"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "중등구조 상",
|
||||
"carpenter": 13.767,
|
||||
"laborer": 1.497,
|
||||
"examples": [
|
||||
"통나무 골막이"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "상등구조",
|
||||
"carpenter": 16.975,
|
||||
"laborer": 1.848,
|
||||
"examples": [
|
||||
"통나무 사방댐"
|
||||
]
|
||||
}
|
||||
],
|
||||
"type_map": [
|
||||
{
|
||||
"type_id": "soil_guard",
|
||||
"class": "보통구조 상",
|
||||
"matched": "통나무 기슭막이",
|
||||
"provisional": true,
|
||||
"why": "임도 흙막이는 통나무를 짜 맞춘 틀이고 **소각재·박판·소폭판이 목재의 50 % 이상**이라는 상등구조 조건을 안 채운다. 원문 예시로도 「통나무 기슭막이」가 같은 급이고 「통나무 사방댐」은 사방 구조물이다.",
|
||||
"compare": "상등구조를 쓰면 건축목공 8.760 → 16.975 인/㎥ 로 약 1.9배"
|
||||
}
|
||||
],
|
||||
"pending_user": {
|
||||
"note": "⚠ 위 판정은 **잠정**이다. 사용자가 예/아니오로 답할 수 있게 물음을 좁혀 둔다.",
|
||||
"question": "임도 흙막이의 목재공작물 구조 갈래가 「보통구조 상(통나무 기슭막이 급)」이 맞습니까?"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user