Merge remote-tracking branch 'origin/main_desktop_1' into sub_desktop_1
This commit is contained in:
@@ -407,6 +407,29 @@ def formula_rows(table: dict[str, Any]) -> list[str]:
|
||||
return found
|
||||
|
||||
|
||||
# ⚠ **규격 표기에 쓰이는 특수문자** — 원문이 한 종류로 안 쓴다(2026-09-07 실측).
|
||||
# 물결표만 셋이다: `∼`(U+223C) 662회 · `~`(U+FF5E) 459회 · `~`(U+007E) 2회.
|
||||
# 곱셈표도 `×`(U+00D7) 97회 · `x`(U+0078) 4회로 갈린다.
|
||||
# **두 창이 각자 갈래 키를 조립하면 글자 하나로 영영 안 맞는다** — 그래서 우리는
|
||||
# 키를 조립하지 않고 **저장 원본값만** 보낸다(인계 계약). 여기서는 **표시만** 한다.
|
||||
# ⚠ 값을 고치지 않는다 — 원문 표기를 흡수하는 것은 **원문을 읽는 쪽**의 몫이다.
|
||||
SPECIAL_GLYPHS = {
|
||||
"∼": "∼(U+223C)",
|
||||
"~": "~(U+FF5E)",
|
||||
"~": "~(U+007E)",
|
||||
"×": "×(U+00D7)",
|
||||
"x": "x(U+0078)",
|
||||
"–": "–(U+2013)",
|
||||
"‐": "‐(U+2010)",
|
||||
}
|
||||
|
||||
|
||||
def special_glyphs(table: dict[str, Any]) -> list[str]:
|
||||
"""규격 표기에 쓰인 특수문자 종류. 갈래 키를 맞출 때 대조할 자리."""
|
||||
text = " ".join(norm(cell) for row in table.get("rows", []) for cell in row)
|
||||
return sorted({SPECIAL_GLYPHS[ch] for ch in text if ch in SPECIAL_GLYPHS})
|
||||
|
||||
|
||||
def crew_table(table: dict[str, Any]) -> bool:
|
||||
"""작업조 + 시공량으로 적힌 표인가."""
|
||||
hay = " ".join(norm(h) for h in table.get("headers", []))
|
||||
@@ -532,6 +555,8 @@ def build() -> dict[str, Any]:
|
||||
"spaced_names": spaced_names(table),
|
||||
# ⚠ 공식 기호 줄 — 소요량이 아니다. 자원으로 세면 이중계상.
|
||||
"formula_rows": formula_rows(table),
|
||||
# ⚠ 규격 표기 특수문자 — 갈래 키를 맞출 때 대조할 자리(값은 안 고침).
|
||||
"special_glyphs": special_glyphs(table),
|
||||
# 공식 기호가 이 표에 직접 있는가 — 없으면 앞 표에서 물려받는 모양이다.
|
||||
"capacity_formula_here": capacity_formula_pending(table),
|
||||
"variant_key": variant_axis(table),
|
||||
|
||||
@@ -94,7 +94,7 @@ def _split_by_rock(total: float, source: SummaryInput) -> list[tuple[str, float,
|
||||
given = sum(ratios.values())
|
||||
if given <= 0:
|
||||
return [("암", total, "")]
|
||||
note = "" if abs(given - 100.0) < 1e-9 else f"입력 합 {given:g} % → 100 % 로 안분"
|
||||
note = "" if abs(given - 100.0) < 1e-9 else f"암 갈래 입력 합 {given:g} % → 100 % 로 안분"
|
||||
return [(name, total * ratios[name] / given, note) for name in classes if ratios[name] > 0]
|
||||
|
||||
|
||||
|
||||
@@ -499,6 +499,9 @@ def _earthwork_rows(
|
||||
"spec_detail": "",
|
||||
"composite_parts": None,
|
||||
"structure_kind": None,
|
||||
# 토공·운반 줄에는 갈래 축이 없다 — 그래도 **칸은 둔다**(계약이 한 모양).
|
||||
"variant_axis": None,
|
||||
"variant_value": None,
|
||||
"spec_class": None,
|
||||
"spec_class_basis": "",
|
||||
# 토공 줄은 막힐 자리가 없다 — 그래도 **칸은 둔다**(계약이 한 모양이어야 한다).
|
||||
@@ -551,6 +554,9 @@ def _haul_rows(
|
||||
"spec_detail": "",
|
||||
"composite_parts": None,
|
||||
"structure_kind": None,
|
||||
# 토공·운반 줄에는 갈래 축이 없다 — 그래도 **칸은 둔다**(계약이 한 모양).
|
||||
"variant_axis": None,
|
||||
"variant_value": None,
|
||||
"spec_class": None,
|
||||
"spec_class_basis": "",
|
||||
"blocked_kind": None,
|
||||
@@ -602,13 +608,26 @@ def _structure_rows(
|
||||
type_id = str(structure.get("type_id") or "")
|
||||
entry = mapping.for_structure(type_id) or {}
|
||||
code = entry.get("work_item_code")
|
||||
# 돌쌓기는 **뒷길이 갈래**로 단가가 갈린다 — 저장 제원에서 자동으로 고른다.
|
||||
# 돌쌓기는 **뒷길이 갈래**로, 큰돌쌓기는 **메/찰**로 단가가 갈린다 —
|
||||
# 둘 다 저장 제원에서 자동으로 고른다(사용자 칸을 따로 만들지 않는다).
|
||||
class_key: str | None = None
|
||||
class_basis = ""
|
||||
if entry.get("class_from") == "bond":
|
||||
bond = str((structure.get("options") or {}).get("bond") or "").strip()
|
||||
bond_codes = entry.get("bond_codes") or {}
|
||||
if bond in bond_codes:
|
||||
# 메/찰은 **의미 판정**이라 우리 몫이다 — 공종 자체가 갈린다.
|
||||
code = bond_codes[bond]
|
||||
class_key = bond
|
||||
class_basis = f"쌓기 방식 「{bond}」 → 품셈 13-6 {code.split('-')[-1]}"
|
||||
else:
|
||||
class_basis = (
|
||||
"큰돌쌓기 쌓기 방식이 아직 입력되지 않았습니다 — 구조물 상세 입력에서 "
|
||||
"메쌓기·찰쌓기 중 하나를 고르면 공종이 정해집니다"
|
||||
)
|
||||
if code and entry.get("class_from") == "back_length":
|
||||
class_key, class_basis = masonry_class(structure.get("options") or {})
|
||||
if class_key:
|
||||
code = f"{code}#{normalize_kind_key(class_key)}"
|
||||
|
||||
composite = mapping.composite_for(type_id) if code is None else None
|
||||
kind = structure_kind(structure) if composite else None
|
||||
parts: list[dict[str, Any]] | None = None
|
||||
@@ -616,9 +635,12 @@ def _structure_rows(
|
||||
if composite:
|
||||
parts, parts_missing = composite_quantities(structure, composite, mapping)
|
||||
blocked_kind, blocked_reason = blocked_of(structure, class_basis)
|
||||
# 갈래 축과 **저장 제원 원본값**. 가공하지 않는다.
|
||||
variant_axis = str(entry.get("variant_axis") or "") or None
|
||||
variant_value = (structure.get("options") or {}).get(variant_axis) if variant_axis else None
|
||||
if code is None and composite is None:
|
||||
unmatched.append(f"{wording_type_label(type_id)} — 품셈 공종을 아직 못 이었습니다")
|
||||
elif entry.get("class_from") == "back_length" and class_key is None:
|
||||
elif entry.get("class_from") in ("back_length", "bond") and class_key is None:
|
||||
unmatched.append(f"{wording_type_label(type_id)} — {class_basis}")
|
||||
length = float(structure.get("length_m") or 0.0)
|
||||
rows.append(
|
||||
@@ -647,6 +669,12 @@ def _structure_rows(
|
||||
"blocked_kind": blocked_kind,
|
||||
"blocked_reason": blocked_reason,
|
||||
# 규격 갈래(뒷길이 …㎝ 이하) — 못 고르면 사유가 남는다.
|
||||
# ⚠ **갈래 키 문자열을 우리가 조립하지 않는다** (2026-09-07 계약 변경).
|
||||
# 품셈 원문이 물결표를 섞어 쓴다(`∼` U+223C / `~` U+FF5E). 두 창이 각자
|
||||
# 키를 조립하면 **글자 하나로 영영 안 맞는다.** 우리는 **어느 축인지와
|
||||
# 저장 원본값**만 보내고, 원문을 읽는 쪽이 그 표기를 흡수한다.
|
||||
"variant_axis": variant_axis,
|
||||
"variant_value": variant_value,
|
||||
"spec_class": class_key,
|
||||
"spec_class_basis": class_basis,
|
||||
# ⚠ 물량을 못 채운 조각 — 0 으로 적지 않고 사유와 함께 드러낸다.
|
||||
@@ -747,7 +775,11 @@ def build_handoff(
|
||||
"excluded_row_count": sum(1 for row in work_items if not row["in_bill"]),
|
||||
}
|
||||
# ⚠ 검사는 **실제로 부른다** — 만들어 두고 안 부르면 없는 것과 같다.
|
||||
# 2026-09-08 ㉘ 자기 감사: 아래 줄 하나만 이어져 있고 형제 둘은 **시험에서만** 불리고
|
||||
# 있었다. B09 에서 같은 병(가드 둘이 놀고 있음)을 지적해 놓고 내 쪽도 같았다.
|
||||
result["ratio_math_warnings"] = verify_ratio_math(result)
|
||||
result["material_code_warnings"] = verify_no_code_on_materials(result)
|
||||
result["bill_flag_warnings"] = verify_bill_flags(result)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any, Iterable
|
||||
|
||||
from B08_Quantity.B08_Quantity_Wording import type_label
|
||||
|
||||
#: 사방 시설로 보는 구조물 종류 — **레지스트리의 실제 `type_id` 를 쓴다**(D 그룹 + 흙막이).
|
||||
#: 목록에 없으면 그 노선에 사방공이 **없는** 것이다. 이름을 지어내면 영영 안 걸린다.
|
||||
EROSION_CONTROL_TYPES = frozenset(
|
||||
@@ -72,13 +74,23 @@ def batter_frame_count(slope_rows: Iterable[dict[str, Any]]) -> tuple[int, list[
|
||||
return count, notes
|
||||
|
||||
|
||||
def level_frame_count(slope_rows: Iterable[dict[str, Any]]) -> tuple[int, list[str]]:
|
||||
"""수평 규준틀 — **중심점 성토 높이 5m 이상**에 설치(품셈 11-3 [주]①).
|
||||
def level_frame_count(slope_rows: Iterable[dict[str, Any]]) -> tuple[int | None, list[str]]:
|
||||
"""수평 규준틀 — **성토고 5m 이상 측점마다** 한 개소(품셈 11-3 [주]①).
|
||||
|
||||
⚠ 성토고는 사면표에 없다 — 그 값을 받기 전에는 **0 으로 때우지 않고 미확보**로 둔다.
|
||||
⚠ 「비탈길이 10m 이상 20m마다」인 비탈규준틀과 **기준이 다르다** — 이쪽은 **지점 조건**이라
|
||||
간격이 없다. 두 기준을 같은 식으로 쓰면 조용히 틀린다.
|
||||
⚠ 성토고 칸이 아예 없으면 **0 으로 때우지 않고** 미확보로 둔다 — 「없음」과 다르다.
|
||||
"""
|
||||
return 0, [
|
||||
"중심점 성토고 5m 이상 지점에 설치(품셈 11-3 [주]①)인데 성토고가 이 표에 없어 개소를 못 셈"
|
||||
rows = list(slope_rows)
|
||||
if not rows:
|
||||
return None, ["사면표가 없어 성토고를 못 봄"]
|
||||
if all("fill_height_m" not in row for row in rows):
|
||||
return None, ["성토고가 사면표에 없어 개소를 못 셈 (품셈 11-3 [주]① 「성토고 5m 이상」)"]
|
||||
tall = [
|
||||
row for row in rows if float(row.get("fill_height_m") or 0.0) >= LEVEL_MIN_FILL_HEIGHT_M
|
||||
]
|
||||
return len(tall), [
|
||||
f"성토고 {LEVEL_MIN_FILL_HEIGHT_M:g}m 이상 측점 {len(tall)}곳 (품셈 11-3 [주]①)"
|
||||
]
|
||||
|
||||
|
||||
@@ -119,15 +131,7 @@ def preparation_rows(
|
||||
"work_item_code": "FP-09-21",
|
||||
},
|
||||
_batter_frame_row(list(slope_rows)),
|
||||
{
|
||||
"group": "준비공",
|
||||
"item": "수평 규준틀",
|
||||
"unit": "개소",
|
||||
"amount": None,
|
||||
"status": STATUS_PENDING,
|
||||
"reason": level_frame_count(())[1][0],
|
||||
"work_item_code": "FP-11-03",
|
||||
},
|
||||
_level_frame_row(list(slope_rows)),
|
||||
]
|
||||
|
||||
|
||||
@@ -179,8 +183,35 @@ def _batter_frame_row(slope_rows: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def erosion_rows(structures: Iterable[dict[str, Any]] = ()) -> list[dict[str, Any]]:
|
||||
"""사방공 줄 — 이 노선에 사방 시설이 **있을 때만** 값이 선다."""
|
||||
def _level_frame_row(slope_rows: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
"""수평 규준틀 한 줄. 성토고를 못 보면 미확보, 보면 개소가 선다."""
|
||||
count, notes = level_frame_count(slope_rows)
|
||||
return {
|
||||
"group": "준비공",
|
||||
"item": "수평 규준틀",
|
||||
"unit": "개소",
|
||||
"amount": float(count) if count is not None else None,
|
||||
"status": STATUS_READY if count is not None else STATUS_PENDING,
|
||||
"reason": "; ".join(notes)
|
||||
+ (
|
||||
" · 재료량은 품셈 11-3 [주]④ 「설계수량에 따른다」라 미확보"
|
||||
if count is not None
|
||||
else ""
|
||||
),
|
||||
"work_item_code": "FP-11-03",
|
||||
}
|
||||
|
||||
|
||||
def erosion_rows(
|
||||
structures: Iterable[dict[str, Any]] = (),
|
||||
names: dict[str, str] | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""사방공 줄 — 이 노선에 사방 시설이 **있을 때만** 값이 선다.
|
||||
|
||||
⚠ 줄 이름은 **레지스트리 이름**을 쓴다 — `type_id` 를 그대로 적으면 화면에
|
||||
`soil_guard` 같은 개발자 키가 뜬다(B08 ㉑ 과 같은 병). `names` 가 없으면
|
||||
문구표가 받아 주고, 그것도 없으면 키를 보이되 **지어내지는 않는다**.
|
||||
"""
|
||||
found = sorted(
|
||||
{
|
||||
str(item.get("type_id"))
|
||||
@@ -203,7 +234,8 @@ def erosion_rows(structures: Iterable[dict[str, Any]] = ()) -> list[dict[str, An
|
||||
return [
|
||||
{
|
||||
"group": "사방공",
|
||||
"item": type_id,
|
||||
"item": type_label(type_id, names),
|
||||
"type_id": type_id,
|
||||
"unit": "개소",
|
||||
"amount": None,
|
||||
"status": STATUS_PENDING,
|
||||
@@ -219,10 +251,11 @@ def build_table(
|
||||
structures: Iterable[dict[str, Any]] = (),
|
||||
slope_rows: Iterable[dict[str, Any]] = (),
|
||||
topsoil_thickness_m: float | None = None,
|
||||
names: dict[str, str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""화면·인계가 그대로 쓰는 모양. **못 서는 줄도 목록에 남는다.**"""
|
||||
rows = preparation_rows(slope_totals, slope_rows, topsoil_thickness_m) + erosion_rows(
|
||||
structures
|
||||
structures, names
|
||||
)
|
||||
return {
|
||||
"columns": ["구분", "공종", "단위", "수량", "상태", "사유"],
|
||||
|
||||
@@ -69,6 +69,8 @@ class SlopeAreaRow:
|
||||
chainage_m: float
|
||||
distance_m: float = 0.0
|
||||
berm_width_m: float = 0.0
|
||||
# 성토고(m) — 수평 규준틀 개소 판정(품셈 11-3 [주]① 「성토고 5m 이상」)이 쓴다.
|
||||
fill_height_m: float = 0.0
|
||||
unclosed: bool = False
|
||||
lengths: dict[str, float] = field(default_factory=dict)
|
||||
areas: dict[str, float] = field(default_factory=dict)
|
||||
@@ -102,6 +104,7 @@ def build_rows(
|
||||
row = SlopeAreaRow(
|
||||
chainage_m=slope.chainage_m,
|
||||
berm_width_m=slope.berm_width_m,
|
||||
fill_height_m=slope.fill_height_m,
|
||||
unclosed=slope.unclosed,
|
||||
)
|
||||
for series, faces in SERIES:
|
||||
@@ -153,6 +156,7 @@ def build_table(
|
||||
"chainage_m": row.chainage_m,
|
||||
"distance_m": row.distance_m,
|
||||
"berm_width_m": row.berm_width_m,
|
||||
"fill_height_m": row.fill_height_m,
|
||||
"unclosed": row.unclosed,
|
||||
"lengths": row.lengths,
|
||||
"areas": row.areas,
|
||||
|
||||
@@ -59,6 +59,10 @@ class StationSlope:
|
||||
cut_length_m: float = 0.0
|
||||
fill_length_m: float = 0.0
|
||||
berm_width_m: float = 0.0
|
||||
# 성토고(m) — 성토 사면 조각들의 **수직 낙차 합**. 노면 끝에서 원지반까지 내려간 높이다.
|
||||
# ⚠ 좌우가 다르면 **큰 쪽**을 쓴다. 「중심점 성토고 5m 이상」(품셈 11-3 [주]①) 판정은
|
||||
# 가장 높은 쪽이 기준이고, 양쪽을 더하면 실제보다 두 배가 된다.
|
||||
fill_height_m: float = 0.0
|
||||
segments: tuple[SlopeSegment, ...] = ()
|
||||
# 사면이 샘플 범위 끝까지 원지반을 못 만나 **면적이 잘린** 측점.
|
||||
# 설계 엔진이 `slope_unclosed` 로 이미 경고하는 값을 그대로 물고 온다. 잘린 측점은
|
||||
@@ -204,10 +208,15 @@ def station_slope(chainage_m: float, design: dict[str, Any]) -> StationSlope:
|
||||
for side in ("left", "right"):
|
||||
segments.extend(_side_segments(design, side, ratios))
|
||||
berm = design.get("berm") or {}
|
||||
fill_by_side = {
|
||||
side: sum(abs(s.rise_m) for s in segments if s.role == "fill" and s.side == side)
|
||||
for side in ("left", "right")
|
||||
}
|
||||
return StationSlope(
|
||||
chainage_m=float(chainage_m),
|
||||
cut_length_m=sum(s.length_m for s in segments if s.role == "cut"),
|
||||
fill_length_m=sum(s.length_m for s in segments if s.role == "fill"),
|
||||
fill_height_m=max(fill_by_side.values(), default=0.0),
|
||||
berm_width_m=_num(berm.get("width_m")) or 0.0,
|
||||
segments=tuple(segments),
|
||||
unclosed=bool(design.get("slope_unclosed")),
|
||||
|
||||
@@ -58,7 +58,14 @@ DEFAULT_BACK_LENGTH_CM = 45
|
||||
|
||||
# 돌쌓기 전개식의 상수 — 실무 수식에 박혀 있던 값을 뺀 것.
|
||||
STONE_MASONRY = {
|
||||
"face_to_slope_factor": 1.04, # 돌쌓기 면적 = 정면적 × 1.04 (비탈 기울기 몫)
|
||||
# ⚠ **곱하는 값이 아니라 검산 참고값이다** (2026-09-08 ㉘ 에서 고침).
|
||||
# 실무 시트의 「돌쌓기 = 정면적 × 1.04」에서 그 1.04 가 **곧 기울기 몫**이다
|
||||
# (1:0.3 → √(1+0.3²) = 1.0440 ≈ 1.04). 시트가 반올림해 적은 것을 우리가
|
||||
# **별도 계수로 오해해 `hypot` 위에 또 곱하고 있었다** — 면적이 4 % 부풀었고
|
||||
# 그 면적이 고임돌·야면석·채움콘크리트·모르터·물구멍 **전부의 밑수**였다.
|
||||
# ⚠ 하드코딩하면 안 되는 값이다 — 큰돌쌓기는 「1:0.3 **이상**」이라 기울기가
|
||||
# 바뀔 수 있고, 그때 1.04 는 틀린 값이 되지만 `hypot` 은 따라간다.
|
||||
"sheet_check_factor_at_0_3": 1.04, # 1:0.3 에서 시트값과 맞는지 대조하는 자리
|
||||
"thickness_base_m": 0.45, # 평균두께 식의 밑돌 두께
|
||||
"thickness_top_coeff": 0.10, # 상부 두께 계수 (0.45 + 0.10·H)
|
||||
"thickness_bottom_coeff": 0.40, # 하부 두께 계수 (0.45 + 0.40·H)
|
||||
@@ -178,7 +185,9 @@ BOULDER_DIAMETERS = ("40~60", "60~80", "80~100")
|
||||
#: 큰돌쌓기 전개 상수. **재료 원단위는 품셈에 없다** — 13-6 [주]⑦ 「재료량은 설계수량을 적용한다」.
|
||||
#: 그래서 여기서 내는 것은 **면적과 터파기 계열까지**이고 큰돌 자체는 미확보로 둔다.
|
||||
BOULDER_MASONRY = {
|
||||
"face_to_slope_factor": 1.04, # 돌쌓기 면적 = 정면적 × 1.04 (13-4 와 같은 기울기 몫)
|
||||
# ⚠ 검산 참고값 — 곱하지 않는다. 까닭은 `STONE_MASONRY` 의 같은 칸 주석을 볼 것.
|
||||
# 큰돌쌓기는 전면 기울기가 「1:0.3 **이상**」이라 특히 하드코딩하면 안 된다.
|
||||
"sheet_check_factor_at_0_3": 1.04,
|
||||
"excavation_extra_m": 0.2, # 터파기 폭 여유
|
||||
"backfill_thickness_m": 0.2, # 되메우기 두께
|
||||
}
|
||||
@@ -221,8 +230,8 @@ def boulder_masonry(
|
||||
slope_ratio = _num(options.get("face_slope_ratio"), 0.3) # 레지스트리에 칸 없음 — 기본 0.3
|
||||
constants = BOULDER_MASONRY
|
||||
face_area = height_m * length_m
|
||||
slope_area = face_area * math.hypot(1.0, slope_ratio)
|
||||
masonry_area = slope_area * constants["face_to_slope_factor"]
|
||||
# 기울기 몫은 **한 번만** — 13-4 와 같은 자리다(㉘).
|
||||
masonry_area = face_area * math.hypot(1.0, slope_ratio)
|
||||
|
||||
components = [
|
||||
Component(
|
||||
@@ -230,7 +239,7 @@ def boulder_masonry(
|
||||
"㎡",
|
||||
masonry_area,
|
||||
DESTINATION["돌쌓기"],
|
||||
f"비탈면적 × 1.04 · 직경 {diameter}㎝ (품셈 13-6)",
|
||||
f"정면적 × √(1+{slope_ratio}²) · 직경 {diameter}㎝ (품셈 13-6)",
|
||||
)
|
||||
]
|
||||
|
||||
@@ -293,13 +302,16 @@ def stone_masonry(
|
||||
# ⚠ `face_slope_ratio` 는 **레지스트리에 없는 키**다 — 즉 지금은 늘 기본 0.3 으로 돈다.
|
||||
# 상수로 두는 것이 아니라 「칸이 생기면 바로 받는다」는 뜻으로 남겨 둔다.
|
||||
# (키 이름 어긋남으로 저장값이 안 닿던 `back_len_cm` 사고와 구별할 것 — 이쪽은 **칸 자체가 없다**.)
|
||||
# **기본 0.3 의 근거** — 교본 7-3 돌흙막이 기준: 「돌 찰쌓기 3.0m 이하 **1:0.3** /
|
||||
# 돌 메쌓기 2.0m 이하 **1:0.3** / 큰돌쌓기 **1:0.3 이상**(전도 방지)」
|
||||
# (`지식DB 02_상세설계/구조물/돌쌓기.md §1`, 값은 `data_masonry` 의 `face_slope`).
|
||||
slope_ratio = _num(options.get("face_slope_ratio"), 0.3) # 전면 기울기 1:0.3 (교본 7-3)
|
||||
constants = STONE_MASONRY
|
||||
|
||||
face_area = height_m * length_m # 정면적
|
||||
# 비탈면적 = 정면적 × √(1+n²) — 기울어진 만큼 길어진다.
|
||||
slope_area = face_area * math.hypot(1.0, slope_ratio)
|
||||
masonry_area = slope_area * constants["face_to_slope_factor"]
|
||||
# 돌쌓기 면적 = 정면적 × √(1+n²) — 기울어진 만큼 길어진다. **기울기 몫은 한 번만.**
|
||||
# 실무 시트의 「정면적 × 1.04」가 바로 이 값이다(1:0.3 에서 1.0440 ≈ 1.04).
|
||||
masonry_area = face_area * math.hypot(1.0, slope_ratio)
|
||||
thickness = (
|
||||
(constants["thickness_base_m"] + constants["thickness_top_coeff"] * height_m)
|
||||
+ (constants["thickness_base_m"] + constants["thickness_bottom_coeff"] * height_m)
|
||||
@@ -307,7 +319,13 @@ def stone_masonry(
|
||||
volume = face_area * thickness # 입적
|
||||
|
||||
components = [
|
||||
Component("돌쌓기", "㎡", masonry_area, DESTINATION["돌쌓기"], "비탈면적 × 1.04"),
|
||||
Component(
|
||||
"돌쌓기",
|
||||
"㎡",
|
||||
masonry_area,
|
||||
DESTINATION["돌쌓기"],
|
||||
f"정면적 × √(1+{slope_ratio}²) — 비탈면적",
|
||||
),
|
||||
Component(
|
||||
"고임돌",
|
||||
"㎥",
|
||||
|
||||
@@ -22,6 +22,7 @@ from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
|
||||
from B06_Section.B06_Section_Repository import (
|
||||
get_cross_section_designs,
|
||||
get_longitudinal_section,
|
||||
@@ -31,6 +32,7 @@ from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import SummaryInput
|
||||
from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import build_table as build_summary_table
|
||||
from B08_Quantity.B08_Quantity_Engine_EarthworkTable import StationArea, build_table
|
||||
from B08_Quantity.B08_Quantity_Engine_HaulSummary import build_table as build_haul_table
|
||||
from B08_Quantity.B08_Quantity_Engine_HaulSummary import check_against_plan
|
||||
from B08_Quantity.B08_Quantity_Engine_Handoff import load_mapping
|
||||
from B08_Quantity.B08_Quantity_Engine_Preparation import build_table as build_preparation_table
|
||||
from B08_Quantity.B08_Quantity_Engine_HaulSummary import summary_input_rows
|
||||
@@ -84,6 +86,17 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse:
|
||||
table["haul"] = haul
|
||||
# 운반계획은 [저장]·[확정]에서 정본에 남는 값이다 — 아직 없으면 빈 표가 정직하다.
|
||||
table["haul_available"] = bool(plan)
|
||||
# ⚠ 검산을 **실제로 부른다** — 무대·도자·덤프 합이 운반계획 총량과 맞는가(8-7 ㉡).
|
||||
# 2026-09-08 ㉘ 자기 감사: 만들어 두고 시험에서만 부르고 있었다. 값을 막지는 않고
|
||||
# 차이만 실어 화면이 띄우게 한다 — 막으면 계획이 없는 정상 상태에서도 멈춘다.
|
||||
if plan:
|
||||
check = check_against_plan(haul, plan)
|
||||
table["haul_check"] = {
|
||||
"hauled_total_m3": check.hauled_total_m3,
|
||||
"plan_total_m3": check.plan_total_m3,
|
||||
"difference_m3": check.difference_m3,
|
||||
"by_equipment": check.details,
|
||||
}
|
||||
|
||||
table["summary"] = build_summary_table(
|
||||
SummaryInput(
|
||||
@@ -99,11 +112,13 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse:
|
||||
)
|
||||
)
|
||||
# 준비공·사방공 — 못 서는 줄도 사유와 함께 남긴다(빈 표는 「빠뜨림」과 구별이 안 됨).
|
||||
structures = await _route_structures(project_id)
|
||||
table["preparation"] = build_preparation_table(
|
||||
slope.get("totals") or {},
|
||||
await _route_structures(project_id),
|
||||
structures,
|
||||
slope.get("rows") or [],
|
||||
settings.get("topsoil_thickness_m"),
|
||||
{type_id: definition.name for type_id, definition in structure_type_map().items()},
|
||||
)
|
||||
method, method_is_default = concrete_placing_method(settings)
|
||||
# ⚠ 금액에 바로 걸리는 값이라 「기본값으로 돌고 있음」을 응답에 실어 화면이 띄우게 한다.
|
||||
@@ -186,6 +201,11 @@ class QuantitySettingsBody(BaseModel):
|
||||
topsoil_thickness_m: float | None = None
|
||||
|
||||
|
||||
#: `None` 이 「안 정함」을 뜻하는 칸 — 저장에서 **버리지 않고 그대로 덮어쓴다**.
|
||||
#: 빈 문자열로 되돌리는 칸(시공법·타설 방식)과 달리 숫자 칸은 되돌릴 값이 `None` 뿐이다.
|
||||
NULLABLE_SETTING_KEYS = ("topsoil_thickness_m",)
|
||||
|
||||
|
||||
@router.put("/{project_id}/quantity/settings")
|
||||
async def save_quantity_settings(project_id: UUID, body: QuantitySettingsBody) -> JSONResponse:
|
||||
"""산출 조건을 정본에 남긴다 — [저장]이 부르는 자리.
|
||||
@@ -203,6 +223,12 @@ async def save_quantity_settings(project_id: UUID, body: QuantitySettingsBody) -
|
||||
content={"status": "error", "message": "프로젝트 저장 폴더를 찾지 못했습니다."},
|
||||
)
|
||||
values = {key: value for key, value in body.model_dump().items() if value is not None}
|
||||
# ⚠ `None` 을 통째로 버리면 **「안 정함」으로 되돌릴 길이 없다** — 한 번 넣은 값이
|
||||
# 영영 남는다(2026-09-08 ㉘ 자기 감사). 시공법·타설 방식은 빈 문자열로 되돌리지만
|
||||
# 숫자 칸은 되돌리는 값이 `None` 뿐이라, **화면이 보낸 것**만 골라 살린다.
|
||||
for key in NULLABLE_SETTING_KEYS:
|
||||
if key in body.model_fields_set:
|
||||
values[key] = getattr(body, key)
|
||||
if "concrete_placing_method" in values:
|
||||
method = values["concrete_placing_method"]
|
||||
# 「안 정함」으로 되돌릴 수 있어야 한다 — 빈 값이면 지운다(8-22 ② 와 같은 자리).
|
||||
@@ -222,7 +248,7 @@ async def save_quantity_settings(project_id: UUID, body: QuantitySettingsBody) -
|
||||
_save_quantity,
|
||||
root,
|
||||
values,
|
||||
("rock_methods", "material_supply", "concrete_placing_method"),
|
||||
("rock_methods", "material_supply", "concrete_placing_method") + NULLABLE_SETTING_KEYS,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("B08 설정 저장 실패(쓰기): project_id=%s", project_id)
|
||||
|
||||
@@ -65,6 +65,8 @@ export interface QuantitySettings {
|
||||
material_supply?: Record<string, { supply: string; install_by: string | null }>;
|
||||
/** 콘크리트 타설 방식. `null`·없음이면 **아직 안 정한 것**이고 화면이 기본값 안내를 띄운다. */
|
||||
concrete_placing_method?: string | null;
|
||||
/** 표토 두께(m). `null`·없음이면 **안 정한 것**이라 표토제거 줄이 「근거 없음」으로 선다. */
|
||||
topsoil_thickness_m?: number | null;
|
||||
}
|
||||
|
||||
export interface EarthworkTable {
|
||||
|
||||
@@ -84,6 +84,8 @@ async function saveQuantitySettings(projectId: string, draft: DraftSettings): Pr
|
||||
rock_methods: draft.rock_methods,
|
||||
material_supply: draft.material_supply,
|
||||
concrete_placing_method: draft.concrete_placing_method,
|
||||
// ⚠ `null` 도 그대로 보낸다 — 「안 정함」으로 되돌릴 길이 있어야 한다(시공법과 같은 규칙).
|
||||
topsoil_thickness_m: draft.topsoil_thickness_m,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -134,6 +136,37 @@ function numberField(label: string, value: number, onInput: (value: number) => v
|
||||
return row;
|
||||
}
|
||||
|
||||
/** 비워 둘 수 있는 숫자 칸 — **빈 값은 「안 정함」**이고 0 과 다르다.
|
||||
*
|
||||
* ⚠ `numberField` 로 두면 빈 칸이 0 으로 읽혀 「두께 0m」와 「안 정함」이 같아진다.
|
||||
* 표토제거처럼 **안 정하면 줄이 아예 안 서는** 값은 그 둘을 갈라야 한다.
|
||||
*/
|
||||
function optionalNumberField(
|
||||
label: string,
|
||||
value: number | null,
|
||||
step: string,
|
||||
onInput: (value: number | null) => void,
|
||||
): HTMLElement {
|
||||
const row = document.createElement("label");
|
||||
row.className = "b08-quantity__field";
|
||||
const name = document.createElement("span");
|
||||
name.textContent = label;
|
||||
const input = document.createElement("input");
|
||||
input.type = "number";
|
||||
input.className = "b08-quantity__input";
|
||||
input.min = "0";
|
||||
input.step = step;
|
||||
input.placeholder = L("B08_Quantity_Unset_Placeholder");
|
||||
input.value = value === null || value === undefined ? "" : String(value);
|
||||
// 자동저장은 만들지 않는다 — 입력은 캐시에만 남는다(CLAUDE.md 5장).
|
||||
input.addEventListener("input", () => {
|
||||
const text = input.value.trim();
|
||||
onInput(text === "" ? null : Number(text));
|
||||
});
|
||||
row.append(name, input);
|
||||
return row;
|
||||
}
|
||||
|
||||
/** 타설 방식 표기 — 코드가 아니라 사람이 읽는 이름으로 보인다. */
|
||||
const PLACING_LABELS: Record<string, string> = {
|
||||
ready_mixed: "레디믹스트",
|
||||
@@ -182,6 +215,8 @@ interface DraftSettings {
|
||||
rock_methods: Record<string, string>;
|
||||
// 콘크리트 타설 방식 — `""` 는 「안 정함」이고 저장에서 지워진다.
|
||||
concrete_placing_method: string;
|
||||
// 표토 두께(m) — `null` 은 「안 정함」. 정해야 표토제거 줄이 선다(품셈 9-15 [주]② 의 T).
|
||||
topsoil_thickness_m: number | null;
|
||||
// 자재별 관급/사급 — 표 안에서 줄마다 고른 값.
|
||||
material_supply: Record<string, SupplyChoice>;
|
||||
dirty: boolean;
|
||||
@@ -218,7 +253,10 @@ function buildQuantitySidePanel(
|
||||
panel.append(field(L("B08_Quantity_Side_RockRatios"), ""));
|
||||
for (const name of classes) {
|
||||
// 「암」은 비율을 넣으면 사라지는 되메움 줄이라 비율 칸을 두지 않는다.
|
||||
if (name !== "암") {
|
||||
// 「토사」도 비율 칸을 두지 않는다 — 토사 물량은 토적표의 흙깎기 값이 그대로 서고,
|
||||
// 비율은 **암 총량을 갈래로 나누는 데만** 쓰인다(집계 엔진 `_rock_split`).
|
||||
// 칸을 두면 넣은 값이 조용히 버려져 「입력 합 60 %」 같은 안내가 뜬다(2026-09-08 통과).
|
||||
if (name !== "암" && name !== "토사") {
|
||||
panel.append(
|
||||
numberField(name, draft.rock_ratios_pct[name] ?? 0, (value) => {
|
||||
draft.rock_ratios_pct[name] = value;
|
||||
@@ -262,6 +300,22 @@ function buildQuantitySidePanel(
|
||||
}
|
||||
}
|
||||
|
||||
// ── 표토 두께 — 정해야 준비공 표토제거 줄이 선다(품셈 9-15 [주]② 의 「T : 표토두께(m)」) ──
|
||||
// ⚠ 2026-09-08 ㉘ 자기 감사: 서버·엔진은 이 값을 받고 있었는데 **화면에 넣을 칸이 없었다.**
|
||||
// 「죽은 칸」(넣어도 안 쓰임)의 반대 짝이다 — 쓰이는데 넣을 데가 없던 자리.
|
||||
panel.append(field(L("B08_Quantity_Side_Topsoil"), ""));
|
||||
panel.append(
|
||||
optionalNumberField(
|
||||
L("B08_Quantity_Side_Topsoil_Label"),
|
||||
draft.topsoil_thickness_m,
|
||||
"0.01",
|
||||
(value) => {
|
||||
draft.topsoil_thickness_m = value;
|
||||
draft.dirty = true;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// ── 콘크리트 타설 방식 — ⚠ **금액에 바로 걸리는 값**이라 잠정임을 조용히 두지 않는다 ──
|
||||
panel.append(field(L("B08_Quantity_Side_Placing"), ""));
|
||||
panel.append(
|
||||
@@ -298,6 +352,13 @@ function buildQuantitySidePanel(
|
||||
placing.is_default ? `${label} (${L("B08_Quantity_Placing_Default_Tag")})` : label,
|
||||
),
|
||||
);
|
||||
// ⚠ 고른 값이 **아직 금액에 안 닿는다**는 사실을 숨기지 않는다(2026-09-08 ㉘).
|
||||
// 인계가 타설 공종 줄을 아직 안 세운다 — B09 일위대가가 타설 품을 이미 갖고 있는지
|
||||
// 확인되기 전까지는 세우면 같은 콘크리트를 두 번 센다(이중계상 ㉢ 과 같은 자리).
|
||||
const pending = document.createElement("p");
|
||||
pending.className = "b08-quantity__note";
|
||||
pending.textContent = L("B08_Quantity_Placing_NotApplied");
|
||||
panel.append(pending);
|
||||
}
|
||||
// ⚠ 「정하면 얼마나 달라지는지」까지 보여야 사용자가 판단한다. 이 값은 **참고 표시 전용**이고
|
||||
// B08 의 어떤 계산에도 안 들어간다(금액은 B09 몫).
|
||||
@@ -500,6 +561,7 @@ export async function renderB08Quantity(root: HTMLElement): Promise<void> {
|
||||
application_ratios_pct: { ...(stored.application_ratios_pct ?? {}) },
|
||||
rock_methods: { ...((stored.rock_methods ?? {}) as Record<string, string>) },
|
||||
concrete_placing_method: (stored.concrete_placing_method as string) ?? "",
|
||||
topsoil_thickness_m: (stored.topsoil_thickness_m as number | null) ?? null,
|
||||
material_supply: { ...((stored.material_supply ?? {}) as Record<string, SupplyChoice>) },
|
||||
dirty: false,
|
||||
};
|
||||
|
||||
@@ -30,6 +30,11 @@ TYPE_LABELS = {
|
||||
"ford_pavement": "물넘이포장",
|
||||
"ford_bridge": "세월교",
|
||||
"box_culvert": "BOX암거",
|
||||
# 사방 시설 — 레지스트리 이름 그대로 옮긴 것(2026-09-08 확인). 지어낸 이름 아님.
|
||||
"erosion_check": "골막이",
|
||||
"bed_sill": "바닥막이",
|
||||
"check_dam_small": "소형사방댐(복합형)",
|
||||
"revetment": "기슭막이",
|
||||
}
|
||||
|
||||
#: 저장 제원 칸의 사람 이름 + **어디서 채우는지**. 키 이름을 화면에 내보내지 않기 위한 표.
|
||||
|
||||
@@ -31,5 +31,24 @@
|
||||
"60~80",
|
||||
"80~100"
|
||||
]
|
||||
},
|
||||
"face_slope": {
|
||||
"note": "전면 기울기(1:n) — **교본 7-3 돌흙막이 기준**이 형식별로 정해 둔다. 코드의 기본 0.3 은 지어낸 값이 아니라 이 표에서 온 것이다.",
|
||||
"source": "resources/knowledge/technical_info/01_임도/02_상세설계/구조물/돌쌓기.md §1 (교본 7-3)",
|
||||
"quote": "돌 찰쌓기 3.0m 이하 1:0.3 / 돌 메쌓기 2.0m 이하 1:0.3 / 큰돌쌓기 1:0.3 이상(전도 방지)",
|
||||
"by_type": {
|
||||
"masonry_wet": 0.3,
|
||||
"masonry_dry": 0.3,
|
||||
"boulder_masonry": 0.3
|
||||
},
|
||||
"pending": "⚠ 큰돌쌓기는 「1:0.3 **이상**」이라 더 완만하게 잡을 수 있음 — 칸을 만든다면 그 범위를 보여야 함. 지금은 하한 0.3 으로 감."
|
||||
},
|
||||
"bond": {
|
||||
"note": "큰돌쌓기 쌓기 방식 — 저장 제원 `bond`. 품셈 13-6-1(메)·13-6-2(찰)로 그대로 갈린다.",
|
||||
"option_key": "bond",
|
||||
"codes": {
|
||||
"메쌓기": "FP-13-06-01",
|
||||
"찰쌓기": "FP-13-06-02"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,16 +119,16 @@
|
||||
"work_item_code": "FP-13-04-05",
|
||||
"master_name": "돌쌓기 > 찰쌓기(장비)",
|
||||
"note": "인력 시공이면 FP-13-04-04",
|
||||
"class_from": "back_length",
|
||||
"class_note": "저장 제원 `back_len_cm` 으로 「…㎝ 이하」 구간을 고름 — 자동 판정"
|
||||
"class_note": "⚠ 갈래 키 문자열을 여기서 만들지 않는다 — `variant_axis`+`variant_value`(저장 원본값)만 보내고 「…㎝ 이하」 구간 나누기는 **품셈 원문을 읽는 쪽**이 한다(2026-09-07 계약).",
|
||||
"variant_axis": "back_len_cm"
|
||||
},
|
||||
{
|
||||
"type_id": "masonry_dry",
|
||||
"work_item_code": "FP-13-04-02",
|
||||
"master_name": "돌쌓기 > 메쌓기(장비)",
|
||||
"note": "인력 시공이면 FP-13-04-01",
|
||||
"class_from": "back_length",
|
||||
"class_note": "저장 제원 `back_len_cm` 으로 「…㎝ 이하」 구간을 고름 — 자동 판정"
|
||||
"class_note": "⚠ 갈래 키 문자열을 여기서 만들지 않는다 — `variant_axis`+`variant_value`(저장 원본값)만 보내고 「…㎝ 이하」 구간 나누기는 **품셈 원문을 읽는 쪽**이 한다(2026-09-07 계약).",
|
||||
"variant_axis": "back_len_cm"
|
||||
},
|
||||
{
|
||||
"type_id": "pipe_inlet_basin",
|
||||
@@ -139,6 +139,17 @@
|
||||
"type_id": "ford_pavement",
|
||||
"work_item_code": "FP-12-06",
|
||||
"master_name": "콘크리트 포장(인력시공)"
|
||||
},
|
||||
{
|
||||
"type_id": "boulder_masonry",
|
||||
"work_item_code": null,
|
||||
"class_from": "bond",
|
||||
"bond_codes": {
|
||||
"메쌓기": "FP-13-06-01",
|
||||
"찰쌓기": "FP-13-06-02"
|
||||
},
|
||||
"class_note": "메/찰(`bond`)은 **공종 자체가 갈리는 의미 판정**이라 여기서 고른다. 직경 갈래는 `variant_value` 로 원본값만 보낸다 — 원문이 물결표를 섞어 써서.",
|
||||
"variant_axis": "stone_cm"
|
||||
}
|
||||
],
|
||||
"pending_user": {
|
||||
@@ -160,15 +171,6 @@
|
||||
"FP-09-05 발파암"
|
||||
],
|
||||
"why": "설계자가 넣는 암 갈래 이름(풍화암·연암·보통암·경암)이 리핑이냐 발파냐를 말하지 않음. 갈래마다 시공법을 지정하는 칸이 필요함"
|
||||
},
|
||||
{
|
||||
"type_id": "boulder_masonry",
|
||||
"candidates": [
|
||||
"FP-13-06-01 메쌓기",
|
||||
"FP-13-06-02 찰쌓기"
|
||||
],
|
||||
"why": "저장 제원(`boulder_masonry`)에 **메/찰 구분 칸이 없어** 어느 쪽인지 못 고름. 레지스트리(`B05_Profile_Structure_Types.json`)는 다른 창 소관이라 여기서 못 고침 — 칸이 생기면 `structure` 에 옮긴다.",
|
||||
"class_axis": "직경(`stone_cm`) 40~60·60~80·80~100 — 품셈 13-6 축과 글자까지 같음"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -263,5 +265,17 @@
|
||||
"hand_mixed": 408327
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"variant_contract": {
|
||||
"note": "갈래 계약(2026-09-07 3자) — **키 문자열을 두 창이 각자 조립하지 않는다.**",
|
||||
"why": "품셈 원문이 물결표를 섞어 쓴다: 13-06-01·02 는 `∼`(U+223C), 13-06-03 은 `~`(U+FF5E). 각자 조립하면 글자 하나로 영영 안 맞는다.",
|
||||
"b08_sends": [
|
||||
"work_item_code",
|
||||
"variant_axis",
|
||||
"variant_value",
|
||||
"kind_basis"
|
||||
],
|
||||
"b09_does": "원문 표기(물결표·공백·괄호)를 흡수해 자기 키로 옮긴다. 「…㎝ 이하」 구간 나누기도 그쪽 몫 — 그 구간이 **품셈 표의 구조**이기 때문."
|
||||
},
|
||||
"masonry_class_reference": "resources/data_masonry/masonry_class_2026-01-01.json — ⚠ 이제 **참고용**이다. 서브 판정과 어긋나면 그것이 곧 신호다."
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"dataset_id": "data_work_item_master_manifest",
|
||||
"generated_at": "2026-09-08T01:52:34+09:00",
|
||||
"generated_at": "2026-09-08T04:57:04+09:00",
|
||||
"built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py",
|
||||
"source": {
|
||||
"dataset_id": "pum_forest",
|
||||
@@ -12,8 +12,8 @@
|
||||
"files": [
|
||||
{
|
||||
"file": "work_item_master_2026-01-01.json",
|
||||
"sha256": "08b0c7c26c4aa569ace3b13490b8cbd00606254abb1439894eac750d387bfb03",
|
||||
"size_bytes": 839464
|
||||
"sha256": "1c49012f9f50d721676ad8def15c87c96b77aad83123286dd6981b7d9cefcd81",
|
||||
"size_bytes": 856450
|
||||
},
|
||||
{
|
||||
"file": "form_undetermined_2026-01-01.json",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -628,6 +628,13 @@ export const ui_locales_b2 = {
|
||||
B08_Quantity_Method_Unset: ["안 정함", "Not set"],
|
||||
B08_Quantity_Method_Ripping: ["긁어내기(암절취)", "Ripping"],
|
||||
B08_Quantity_Method_Blasting: ["터뜨리기(발파암)", "Blasting"],
|
||||
B08_Quantity_Placing_NotApplied: [
|
||||
"⚠ 이 선택은 아직 금액에 반영되지 않습니다 — 견적의 일위대가가 타설 품을 이미 갖고 있는지 확인 중입니다.",
|
||||
"Not yet applied to cost — checking whether the unit-price already covers placing labour.",
|
||||
],
|
||||
B08_Quantity_Side_Topsoil: ["표토제거", "Topsoil Removal"],
|
||||
B08_Quantity_Side_Topsoil_Label: ["표토 두께(m)", "Topsoil thickness (m)"],
|
||||
B08_Quantity_Unset_Placeholder: ["안 정함", "Not set"],
|
||||
B08_Quantity_Side_Placing: ["콘크리트 타설", "Concrete Placing"],
|
||||
B08_Quantity_Side_Placing_Label: ["타설 방식", "Method"],
|
||||
B08_Quantity_Placing_Unset: ["안 정함(기본값 사용)", "Not set (default)"],
|
||||
@@ -657,7 +664,10 @@ export const ui_locales_b2 = {
|
||||
B08_Quantity_Ratio_SeedCut: ["초류종자살포(절토면)", "Seed spray (cut)"],
|
||||
B08_Quantity_Ratio_TreeRemoval: ["지장목제거", "Obstacle removal"],
|
||||
B08_Quantity_Side_RockSet: ["암 갈래 세트", "Rock class set"],
|
||||
B08_Quantity_Side_RockRatios: ["지반 구성비(%)", "Ground composition (%)"],
|
||||
B08_Quantity_Side_RockRatios: [
|
||||
"암 갈래 구성비(%) — 암 총량 기준",
|
||||
"Rock class split (%) of rock volume",
|
||||
],
|
||||
B08_Quantity_Btn_Save: ["저장", "Save"],
|
||||
B08_Quantity_Save_Success: ["산출 조건을 저장했습니다.", "Calculation settings saved."],
|
||||
B08_Quantity_Save_Failed: ["산출 조건을 저장하지 못했습니다.", "Failed to save the settings."],
|
||||
|
||||
Reference in New Issue
Block a user