Merge remote-tracking branch 'origin/dev' into main_laptop_1
This commit is contained in:
@@ -240,6 +240,55 @@
|
||||
"default": 2.0,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "length_m",
|
||||
"label": "연장(계류 방향)",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"empty_means": "비우면 수량이 안 섬 — 구체 길이는 설계자 입력(기본값 없음 · 2026-09-15 브레인 판정 ③)"
|
||||
},
|
||||
{
|
||||
"key": "wall_thickness_m",
|
||||
"label": "측벽 두께",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 0.3,
|
||||
"default_basis": "제안값 · 산림과임업기술(임도) 5장 수량산출 예제 벽 0.3 · KDS 44 90 00 4.5.2 도로암거 부재 최소두께 300㎜ · 바꿀 수 있음",
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "slab_thickness_m",
|
||||
"label": "상·저판 두께",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 0.25,
|
||||
"default_basis": "제안값 · 산림과임업기술(임도) 5장 수량산출 예제 판 0.25 — ⚠ KDS 44 90 00 4.5.2 도로암거 부재 최소두께 300㎜ 에 못 미침(임도 예제 0.25 · KDS 도로 최소 300㎜) · 설계자가 고름",
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "haunch_m",
|
||||
"label": "헌치(모따기) 크기",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 0.2,
|
||||
"default_basis": "제안값 · 산림과임업기술(임도) 5장 수량산출 예제 0.2×0.2 · 실무 울진 기번3 구조도 2×2 도 0.2 · 바꿀 수 있음",
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "blinding_thickness_m",
|
||||
"label": "기초(버림) 두께",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 0.1,
|
||||
"default_basis": "제안값 · 산림과임업기술(임도) 5장 수량산출 예제 기초 0.1(양쪽 0.1 여유) · 실무 울진 기번3 구조도 2×2 버림 0.1 · 바꿀 수 있음",
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "wing_in",
|
||||
"label": "날개벽(유입)",
|
||||
|
||||
@@ -110,8 +110,9 @@ FORD_DEFAULT_WIDTH_M = 10.0
|
||||
# 지식DB 폭 수치 근거 없음).
|
||||
FORD_PAVEMENT_DEFAULT_WIDTH_M = 5.0
|
||||
|
||||
# BOX암거 부재 두께(m) — 지식DB에 암거 부재 두께 기준이 없어 세월교 값을 승계한다
|
||||
# (2026-08-25 사용자 확정). 측벽·상판·저판은 각각 벽 두께·바닥판 두께를 그대로 쓴다.
|
||||
# BOX암거 부재 두께 — **등록부 칸**(`wall_thickness_m`·`slab_thickness_m`)이 정본이다
|
||||
# (2026-09-15 브레인 판정 ① · 제안값 = 산림과임업기술(임도) 예제 벽 0.3·판 0.25 · KDS 300㎜ 병기).
|
||||
# 종전 「세월교 값 승계」(2026-08-25 사용자 확정)는 근거 없는 값이라 걷음 — 수량·그림이 한 칸을 읽음.
|
||||
# ⚠ 교차 참조 ④ 암거 위 복토(m) — 별표2 교량·암거 "복토 시 흙 두께 50㎝ 이상".
|
||||
BOX_COVER_M = 0.5
|
||||
|
||||
@@ -451,8 +452,10 @@ def _box_set(options: dict[str, Any] | None) -> dict[str, Any]:
|
||||
values = dict(options or {})
|
||||
inner_width = _number(values.get("body_width_m"), _number(defaults.get("body_width_m"), 2.0))
|
||||
inner_height = _number(values.get("body_height_m"), _number(defaults.get("body_height_m"), 2.0))
|
||||
wall = FORD_WALL_THICKNESS_M
|
||||
slab = FORD_SLAB_THICKNESS_M
|
||||
wall = _number(values.get("wall_thickness_m"), _number(defaults.get("wall_thickness_m"), None))
|
||||
slab = _number(values.get("slab_thickness_m"), _number(defaults.get("slab_thickness_m"), None))
|
||||
wall = wall if wall and wall > 0 else FORD_WALL_THICKNESS_M # 등록부가 비었을 때만(옛 판)
|
||||
slab = slab if slab and slab > 0 else FORD_SLAB_THICKNESS_M
|
||||
wing_in = _wing_spec(values, defaults, "in")
|
||||
wing_out = _wing_spec(values, defaults, "out")
|
||||
return {
|
||||
|
||||
@@ -379,7 +379,7 @@ export function createBoxControls(deps: FordControlDeps): {
|
||||
// 캐시 먼저 — 구체 길이(`span_m`)는 백엔드 `_box_set`과 같은 식으로 다시 잡는다.
|
||||
if (patch.body_width_m) {
|
||||
spec.inner_width_m = patch.body_width_m;
|
||||
spec.span_m = boxSpanM(patch.body_width_m);
|
||||
spec.span_m = boxSpanM(patch.body_width_m, spec.wall_thickness_m);
|
||||
}
|
||||
if (patch.body_height_m) spec.inner_height_m = patch.body_height_m;
|
||||
}
|
||||
|
||||
@@ -369,6 +369,36 @@ def _extra_walls_at(extra_walls: dict[float, list[dict]] | None, chainage: float
|
||||
return extra_walls[best] if abs(best - chainage) <= SECTION_MATCH_TOLERANCE_M else []
|
||||
|
||||
|
||||
#: BOX암거 치수 칸 — 안 적으면 등록부 기본값(제안값)으로 서되 **미확정**(2026-09-14 브레인 판정 ·
|
||||
#: 2026-09-15 12장 D). 연장은 기본값이 없어 여기 없음 — 비면 전개식이 사유를 냄.
|
||||
BOX_DIMENSIONS = (
|
||||
("body_width_m", "본체 폭"),
|
||||
("body_height_m", "본체 높이"),
|
||||
("wall_thickness_m", "측벽 두께"),
|
||||
("slab_thickness_m", "상·저판 두께"),
|
||||
("haunch_m", "헌치"),
|
||||
("blinding_thickness_m", "기초(버림) 두께"),
|
||||
)
|
||||
UNCONFIRMED_BOX = "BOX암거 치수({filled})가 시설 지점에 없어 기본값으로 섰음 — 적으면 금액이 섬"
|
||||
|
||||
|
||||
def _box_row(base: dict[str, Any], registry: dict[str, Any]) -> dict[str, Any]:
|
||||
"""BOX암거 줄 — 빈 치수 칸에 등록부 제안값을 채우고 그 사실을 미확정 사유로 붙임."""
|
||||
options = dict(base["options"])
|
||||
filled = []
|
||||
for key, label in BOX_DIMENSIONS:
|
||||
if _blank(options.get(key)) and registry.get(key) is not None:
|
||||
options[key] = registry[key]
|
||||
filled.append(label)
|
||||
reason = UNCONFIRMED_BOX.format(filled=" · ".join(filled)) if filled else ""
|
||||
return {
|
||||
**base,
|
||||
"options": options,
|
||||
"unconfirmed": reason,
|
||||
"notes": [f"⚠ {reason}"] if reason else [],
|
||||
}
|
||||
|
||||
|
||||
def facility_structures(
|
||||
points: list[dict[str, Any]],
|
||||
extra_walls: dict[float, list[dict]] | None = None,
|
||||
@@ -405,8 +435,11 @@ def facility_structures(
|
||||
"end_m": point.get("end_m") if point.get("end_m") is not None else chainage,
|
||||
"options": options,
|
||||
}
|
||||
if facility == "box_culvert":
|
||||
rows.append(_box_row(base, defaults(facility)))
|
||||
continue
|
||||
if facility not in (FACILITY_PIPE, "revetment"):
|
||||
rows.append(base) # BOX암거·물넘이·세월교 — 전개식·관측값이 없으면 사유로 섬
|
||||
rows.append(base) # 물넘이·세월교 — 전개식·관측값이 없으면 사유로 섬
|
||||
continue
|
||||
legacy = facility == "revetment" # 독립 기슭막이 — 벽 칸 밖의 제원(뒷길이 등)도 벽이 씀
|
||||
wall_defaults = defaults(facility)
|
||||
|
||||
@@ -116,6 +116,8 @@ EXPANDERS = {
|
||||
"soil_guard": lambda h, l, o, f=None, r="": _soil_guard()(o),
|
||||
# 바닥막이는 **돌붙임 ㎡당**이라 높이·연장이 아니라 **면적**이 밑수다(정본 「돌붙임L3=…」).
|
||||
"bed_sill": lambda h, l, o, f=None, r="": _bed_sill()(_num(o.get("area_m2"), 0.0), o),
|
||||
# BOX암거 — m당 × 연장(산림과임업기술 예제 · 울진 2×2 · 2026-09-15 브레인 12장 D).
|
||||
"box_culvert": lambda h, l, o, f=None, r="": _box_culvert()(o),
|
||||
# ⚠⚠ **큰돌쌓기(`boulder_masonry`)를 여기에 두지 않는다** (2026-09-07 발견).
|
||||
# 큰돌쌓기는 품셈 **13-6** 이고 돌쌓기는 **13-4** 다 — **규격 축이 다르다.**
|
||||
# 돌쌓기는 **뒷길이**(35·45·55·60㎝), 큰돌쌓기는 **직경**(40~60·60~80·80~100㎝).
|
||||
@@ -126,6 +128,13 @@ EXPANDERS = {
|
||||
}
|
||||
|
||||
|
||||
def _box_culvert():
|
||||
"""BOX암거 전개 함수를 늦게 가져온다."""
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Box import box_culvert
|
||||
|
||||
return box_culvert
|
||||
|
||||
|
||||
def _soil_guard():
|
||||
"""흙막이 사유 함수를 늦게 가져온다."""
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Revetment import soil_guard
|
||||
@@ -162,7 +171,13 @@ def _revetment():
|
||||
|
||||
|
||||
#: 전개식을 일부러 안 두는 종류 — 왜 안 두는지 사람이 읽게 적는다.
|
||||
EXPANDER_WITHHELD: dict[str, str] = {}
|
||||
EXPANDER_WITHHELD: dict[str, str] = {
|
||||
# 2026-09-15 브레인 판정 ⑥ — 찾아봤으나 없음(「아직 안 만듦」과 다름).
|
||||
"ford_bridge": (
|
||||
"세월교 수량 근거 없음 — 법령·임도기술교본은 설치 조건만(그림 3-18 은 배치 모식도)"
|
||||
" · 실무 원본에 세월교 시트·도면 없음 · 관은 배수관 줄로 따로 섬"
|
||||
),
|
||||
}
|
||||
|
||||
#: 전개식이 **개소당·㎡당**으로 값을 내는 종류의 내역 단위와 수량(2026-09-14 구조물도 그림에서 잡음).
|
||||
#: ⚠ 안 적으면 「m · 연장」으로 읽혀 — 골막이·바닥막이(점 배치라 연장 0)는 **수량 0 이라 내역에 안 서고**
|
||||
@@ -171,6 +186,7 @@ EXPANDER_BILLING: dict[str, Any] = {
|
||||
"erosion_check": lambda options: ("개소", 1.0), # 정본 「골막이(찰)(치수조서연결)」 개소당
|
||||
"soil_guard": lambda options: ("개소", 1.0), # 정본 「떼흙막이」 개소당(평균 붙박이)
|
||||
"bed_sill": lambda options: ("㎡", _num(options.get("area_m2"), 0.0)), # 정본 돌붙임 ㎡당
|
||||
"box_culvert": lambda options: ("m", _num(options.get("length_m"), 0.0)), # 점 배치라 연장 칸
|
||||
}
|
||||
|
||||
|
||||
@@ -328,6 +344,15 @@ def expand(
|
||||
return result
|
||||
|
||||
|
||||
def _shoring_of(quantities: Iterable[StructureQuantity]) -> dict[str, Any]:
|
||||
"""동바리 상태 — 전개식이 동바리 줄을 낸 구조물이 있으면 대상(2026-09-15 BOX암거)."""
|
||||
status = shoring_status()
|
||||
found = sorted({q.name for q in quantities if any(c.name == "동바리" for c in q.components)})
|
||||
if found:
|
||||
status.update(applicable=True, reason=f"슬래브 받침 동바리(12-20) — {' · '.join(found)}")
|
||||
return status
|
||||
|
||||
|
||||
def verify_no_mix_components(quantities: Iterable[StructureQuantity]) -> list[str]:
|
||||
"""⚠ 배합 성분이 산출물에 섞이면 알린다 (㉢ 이중계상 방어).
|
||||
|
||||
@@ -612,8 +637,8 @@ def build_table(
|
||||
"structures": payload_structures,
|
||||
"formwork_notes": formwork_notes,
|
||||
"formwork_reuse_missing": formwork_missing,
|
||||
# 동바리 — 대상이 없으면 0 이 아니라 「없음」이라고 말한다.
|
||||
"shoring": shoring_status(),
|
||||
# 동바리 — 대상이 없으면 0 이 아니라 「없음」이라고 말한다(BOX암거 동바리 줄이 서면 대상).
|
||||
"shoring": _shoring_of(quantities),
|
||||
# ⚠ 값을 바꾸는 설계 조건인데 우리 제원에 칸이 없는 것 — 화면에 드러낸다.
|
||||
# 「무엇을 정해야 하는지」만으로는 부족하고 **「정하면 얼마나 달라지는지」**까지.
|
||||
"pending_choices": (observed.pending_choices or {}).get("items") or [],
|
||||
|
||||
@@ -58,6 +58,10 @@ DESTINATION = {
|
||||
"유로폼": "unit_price", # 거푸집 계열(품셈 12-38) — 설치·해체 품
|
||||
"면목": "material",
|
||||
"떼": "material", # 사면 떼(자재총괄 extra)와 같은 자리 · 할증 10%(1-3-1)
|
||||
# ⭐ 2026-09-15 BOX암거 — 버림 옆면 합판 · 동바리(12-20)·비계(12-19)는 설치 품이라 묶음 조각으로.
|
||||
"합판거푸집": "unit_price",
|
||||
"동바리": "unit_price",
|
||||
"비계": "unit_price",
|
||||
}
|
||||
|
||||
#: ⛔ **갈 곳 기본값 금지**(명세 13장 「빠진 것이 조용해짐」) — 표에 없는 이름은 막고 사유를 냄.
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""BOX암거 전개식 — **m당 × 연장** (2026-09-15 브레인 12장 D 판정 ①~⑤).
|
||||
|
||||
근거 둘 — 구조가 같다
|
||||
① 산림과임업기술(임도) 5장 「나) 구조물도에 의한 수량산출(예)」 · 그림 5-2-70
|
||||
Box(2.0×1.5×2련×6.03m) — 구체 = (외곽 − 유수구 + 헌치) × L · 기초 = 폭 × 0.1 × L ·
|
||||
거푸집 = 외벽·유수구·받침판·귀면 · 동바리 = 유수구 단면 × L · 철근 「상세도에서」(값 없음)
|
||||
② 실무 울진 기번3 `5. 구조도(기번3).xlsx` 「암거수량집계표(2×2)」 m당 — 레미콘 2.84 ·
|
||||
버림 0.28 · 유로폼 11.132 · 합판6회 0.2 · 철근 SD40 H13 0.179 + H16 0.157t · 동바리 3.92 ·
|
||||
비계 18.72
|
||||
두께 제안값은 등록부 칸(예제 벽 0.3 · 판 0.25 · 헌치 0.2 · 버림 0.1 · KDS 300㎜ 병기).
|
||||
⚠ 1련만 — 등록부에 련수 칸이 없음(예제 식은 련수로 늘어나나 그림·칸이 1련).
|
||||
⚠ 철근은 **크기별 식이 없다** — 울진 2×2 관측값만 선다(판정 ②).
|
||||
⚠ 비계 시종점 두 면은 **개소당 한 번** — 원문은 m당 칸에 넣어 연장을 곱하면 부풀어 오름.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Base import Component, _num, routed
|
||||
|
||||
#: 버림 폭 여유(m, 한쪽) — 예제 5.1 = 외곽 4.9 + 0.1×2 · 울진 2.8 = 외곽 2.6 + 0.1×2 (둘이 같음).
|
||||
BLINDING_EXTRA_M = 0.1
|
||||
#: 철근 관측값(kg/m) — 울진 기번3 「암거수량집계표(2×2)」 SD40 · 내공 2.0×2.0 · 벽·판 0.3.
|
||||
REBAR_OBSERVED_2X2 = (("D13", 179.0), ("D16", 157.0))
|
||||
REBAR_SIZE_ONLY = (
|
||||
"철근 — 크기별 식 없음 · 관측값은 2×2 뿐(울진 기번3 구조도 SD40 H13 0.179 + H16 0.157t/m ·"
|
||||
" 벽·판 0.3) — 이 크기는 철근이 안 섬(2026-09-15 브레인 판정 ②)"
|
||||
)
|
||||
SOURCES = (
|
||||
"산식 = 산림과임업기술(임도) 5장 수량산출 예제(그림 5-2-70) · 실무 울진 기번3 구조도"
|
||||
" 「암거수량집계표(2×2)」"
|
||||
)
|
||||
NOTE_FORMWORK = (
|
||||
"거푸집 — 본체 유로폼(외벽 · 내벽 헌치 뺀 높이 · 상판 밑면 · 귀면 넷) · 버림 옆면 합판"
|
||||
" (울진 2×2 모양 · 예제는 합판 3회)"
|
||||
)
|
||||
NOTE_SHORING_SCAFFOLD = (
|
||||
"동바리(유수구 단면 − 헌치)·비계(외벽 양면 + 시종점 두 면)는 **실무 관측 모양**"
|
||||
"(울진 2×2 「강재 3개월」) — 품셈 12-20 강관동바리 · 12-19 강관비계로 이음 ·"
|
||||
" 시종점 두 면은 개소당 한 번(원문은 m당 칸)"
|
||||
)
|
||||
NOTE_WING_EARTH = (
|
||||
"날개벽 수량 근거 없음(교본·실무 모두 · 판정 ⑤) — 날개벽 줄 안 섬"
|
||||
" · 구체 터파기·되메우기는 아직 안 셈"
|
||||
)
|
||||
|
||||
|
||||
def box_culvert(options: dict[str, Any]) -> tuple[list[Component], list[str]]:
|
||||
"""BOX암거 한 개소 — (성분, 사유). 연장이 없으면 성분 없이 사유만."""
|
||||
length = _num(options.get("length_m"))
|
||||
if length <= 0:
|
||||
return [], ["BOX암거 연장(계류 방향)이 없어 수량이 안 섬 — 연장 칸을 적을 것(기본값 없음)"]
|
||||
width, height = _num(options.get("body_width_m")), _num(options.get("body_height_m"))
|
||||
wall, slab = _num(options.get("wall_thickness_m")), _num(options.get("slab_thickness_m"))
|
||||
haunch, blinding = _num(options.get("haunch_m")), _num(options.get("blinding_thickness_m"))
|
||||
if min(width, height, wall, slab) <= 0:
|
||||
return [], ["BOX암거 본체 폭·높이·두께 중 빈 칸이 있어 수량이 안 섬"]
|
||||
|
||||
outer_w, outer_h = width + 2 * wall, height + 2 * slab
|
||||
chamfer = math.sqrt(2) * haunch
|
||||
per_m: list[tuple[str, str, float, str, str]] = [
|
||||
(
|
||||
"콘크리트",
|
||||
"㎥",
|
||||
outer_w * outer_h - width * height + haunch**2 / 2 * 4,
|
||||
f"{outer_w:g}×{outer_h:g} − {width:g}×{height:g} + {haunch:g}²÷2×4",
|
||||
"",
|
||||
),
|
||||
(
|
||||
"버림콘크리트",
|
||||
"㎥",
|
||||
(outer_w + 2 * BLINDING_EXTRA_M) * blinding,
|
||||
f"({outer_w:g} + {BLINDING_EXTRA_M:g}×2) × {blinding:g}",
|
||||
"",
|
||||
),
|
||||
(
|
||||
"유로폼",
|
||||
"㎡",
|
||||
2 * outer_h + 2 * (height - 2 * haunch) + (width - 2 * haunch) + 4 * chamfer,
|
||||
f"외벽 {outer_h:g}×2 + 내벽 ({height:g} − {haunch:g}×2)×2"
|
||||
f" + 상판 밑 ({width:g} − {haunch:g}×2) + 귀면 √({haunch:g}²+{haunch:g}²)×4",
|
||||
"",
|
||||
),
|
||||
("합판거푸집", "㎡", 2 * blinding, f"버림 옆면 {blinding:g}×2", ""),
|
||||
(
|
||||
"동바리",
|
||||
"㎥",
|
||||
width * height - 2 * haunch**2,
|
||||
f"{width:g}×{height:g} − {haunch:g}²÷2×4",
|
||||
"",
|
||||
),
|
||||
("비계", "㎡", 2 * outer_h, f"외벽 양면 {outer_h:g}×2", ""),
|
||||
]
|
||||
notes: list[str] = [SOURCES, NOTE_FORMWORK, NOTE_SHORING_SCAFFOLD, NOTE_WING_EARTH]
|
||||
if math.isclose(width, 2.0) and math.isclose(height, 2.0):
|
||||
per_m += [
|
||||
("이형철근", "kg", kg, "울진 기번3 구조도 2×2 관측(SD40)", size)
|
||||
for size, kg in REBAR_OBSERVED_2X2
|
||||
]
|
||||
notes.append("철근은 관측값(울진 2×2 · 벽·판 0.3) — 두께를 바꿔도 따라 안 바뀜")
|
||||
else:
|
||||
notes.append(REBAR_SIZE_ONLY)
|
||||
|
||||
components = [
|
||||
Component(
|
||||
name,
|
||||
unit,
|
||||
amount * length,
|
||||
destination,
|
||||
f"{basis} = m당 {amount:.4g} × 연장 {length:g}m",
|
||||
spec=spec,
|
||||
)
|
||||
for name, unit, amount, basis, spec in per_m
|
||||
if (destination := routed(name, notes))
|
||||
]
|
||||
ends = 2 * outer_w * outer_h
|
||||
for component in components:
|
||||
if component.name == "비계":
|
||||
component.amount += ends
|
||||
component.basis += (
|
||||
f" + 시종점 두 면 {outer_w:g}×{outer_h:g}×2 = {ends:.4g}(개소당 한 번)"
|
||||
)
|
||||
return components, notes
|
||||
@@ -211,9 +211,9 @@ export function pipeDiameterM(diameterMm: number | null | undefined): number {
|
||||
return round((num(diameterMm, 1000.0) ?? 1000.0) / 1000.0, 3);
|
||||
}
|
||||
|
||||
/** BOX암거 도로 진행 방향 길이 = 내공 폭 + 측벽 두 장. */
|
||||
export function boxSpanM(innerWidthM: number): number {
|
||||
return innerWidthM + 2 * FORD_WALL_THICKNESS_M;
|
||||
/** BOX암거 도로 진행 방향 길이 = 내공 폭 + 측벽 두 장(두께는 등록부 칸 · 2026-09-15). */
|
||||
export function boxSpanM(innerWidthM: number, wallThicknessM: number): number {
|
||||
return innerWidthM + 2 * wallThicknessM;
|
||||
}
|
||||
|
||||
/** 날개벽이 만드는 바닥판 연장량 = 길이 × cos(벌어짐각). 안 세우면 0. */
|
||||
@@ -351,8 +351,11 @@ function boxSet(
|
||||
const defaults = registry.box_culvert ?? {};
|
||||
const innerWidth = num(values.body_width_m, num(defaults.body_width_m, 2.0));
|
||||
const innerHeight = num(values.body_height_m, num(defaults.body_height_m, 2.0));
|
||||
const wall = FORD_WALL_THICKNESS_M;
|
||||
const slab = FORD_SLAB_THICKNESS_M;
|
||||
// 두께는 등록부 칸이 정본(2026-09-15 브레인 12장 D ①) — 파이썬 `_box_set` 과 같은 폴백.
|
||||
const wallValue = num(values.wall_thickness_m, num(defaults.wall_thickness_m, null));
|
||||
const slabValue = num(values.slab_thickness_m, num(defaults.slab_thickness_m, null));
|
||||
const wall = wallValue && wallValue > 0 ? wallValue : FORD_WALL_THICKNESS_M;
|
||||
const slab = slabValue && slabValue > 0 ? slabValue : FORD_SLAB_THICKNESS_M;
|
||||
const wingIn = wingSpec(values, defaults, "in");
|
||||
const wingOut = wingSpec(values, defaults, "out");
|
||||
return {
|
||||
@@ -363,7 +366,7 @@ function boxSet(
|
||||
slab_thickness_m: slab,
|
||||
top_thickness_m: slab,
|
||||
cover_m: BOX_COVER_M,
|
||||
span_m: boxSpanM(innerWidth || 2.0),
|
||||
span_m: boxSpanM(innerWidth || 2.0, wall),
|
||||
wing_in: wingIn,
|
||||
wing_out: wingOut,
|
||||
defaulted: [
|
||||
|
||||
@@ -76,6 +76,12 @@
|
||||
"type_id": "ford_pavement",
|
||||
"reuse_count": null,
|
||||
"note": "물넘이포장은 거푸집이 서지 않는 구조(면 포장) — 대상 아님"
|
||||
},
|
||||
{
|
||||
"type_id": "box_culvert",
|
||||
"reuse_count": 6,
|
||||
"matched_example": "관의 기초",
|
||||
"note": "BOX암거 합판은 **버림 옆면뿐**(본체는 유로폼 — 12-38 유형 「박스」 보통) — 원문 6회 줄 「관의 기초 등 극히 간단한 구조」 · 실무 울진 2×2 「합판6회」 와 같음(2026-09-15)"
|
||||
}
|
||||
],
|
||||
"reuse_ratio_pct": {
|
||||
@@ -99,12 +105,11 @@
|
||||
}
|
||||
},
|
||||
"shoring": {
|
||||
"note": "강관동바리(품셈 12-20)는 **슬래브를 떠받칠 때** 필요하다. 지금 서는 구조물(옹벽·집수정)은 벽체 거푸집만이라 대상이 아니다.",
|
||||
"note": "강관동바리(품셈 12-20)는 **슬래브를 떠받칠 때** 필요하다. 옹벽·집수정은 벽체 거푸집만이라 대상이 아니고, BOX암거는 전개식이 동바리 줄을 낸다(2026-09-15).",
|
||||
"targets_pending": [
|
||||
"box_culvert",
|
||||
"ford_bridge"
|
||||
],
|
||||
"why": "그 둘은 원단위·치수가 미확보라 슬래브 면적 자체가 안 나온다 — 동바리도 함께 미확보"
|
||||
"why": "세월교는 수량 근거가 없어 슬래브 면적 자체가 안 나온다 — 동바리도 함께 미확보"
|
||||
},
|
||||
"euroform_type": {
|
||||
"note": "유로폼 설치·해체 유형(간단/보통/복잡) — **품셈 12-38-3 [주]④ 원문**이 시설 예시로 갈라 둔다. 거푸집 사용횟수(1-7-1)·철근 갈래(12-3 [주]①)와 같은 자리로, 사람이 고르는 값이 아니다.",
|
||||
|
||||
@@ -560,15 +560,10 @@
|
||||
"not_found": {
|
||||
"note": "규격은 우리 모델에 있으나 **관측 원단위가 어디에도 없는** 것. 지어내지 않는다.",
|
||||
"items": [
|
||||
{
|
||||
"type_id": "box_culvert",
|
||||
"why": "울진 2공구에 BOX암거가 실재하나 원단위 라이브러리에 탭이 없음. 게다가 structures.json 의 BOX 제원은 body_width_m·body_height_m 와 날개벽뿐이라 **벽·저판·상판 두께가 없어 전개식도 못 세움**.",
|
||||
"needs": "표준 단면(벽·저판·상판 두께) 확보 — 사용자 확정 대기"
|
||||
},
|
||||
{
|
||||
"type_id": "ford_bridge",
|
||||
"why": "세월교 본체(날개벽 포함) 원단위 없음. 관 부분은 pipe 로 따로 섬.",
|
||||
"needs": "표준도 물량 또는 실무 관측"
|
||||
"why": "세월교 수량 근거 없음 — 법령·임도기술교본은 설치 조건만(그림 3-18 은 배치 모식도) · 실무 원본(울진 4공구·오솔길 전수)에 세월교 시트·도면 없음. 관 부분은 pipe 로 따로 섬(2026-09-15 브레인 판정 ⑥ — 사유로 닫음).",
|
||||
"needs": "세월교 표준도 물량 또는 실무 관측 — 나오면 다시 엶"
|
||||
},
|
||||
{
|
||||
"type_id": "retaining_wall",
|
||||
|
||||
@@ -435,6 +435,62 @@
|
||||
"needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김",
|
||||
"placing_note": "타설 코드는 프로젝트 설정의 타설 방식으로 갈림(기본 레디믹스트). 철근구조물 판정은 원단위의 D13·D16 에서 자동으로 나옴.",
|
||||
"parts_note": "조각마다 **어느 성분에서 나오는지**(`from_components`)를 적는다. 이름을 바꾸면 물량이 조용히 0 이 되므로 원단위 성분 이름과 정확히 같아야 하고, 못 찾으면 드러낸다. `unit_from` 이 있으면 그 단위에서 목표 단위로 환산한다 — **철근은 ㎏ → ton (÷1000)**. ⚠ 단위를 안 맞추면 1000배 틀린다."
|
||||
},
|
||||
{
|
||||
"type_id": "box_culvert",
|
||||
"parts": [
|
||||
{
|
||||
"code": "FP-12-01-01",
|
||||
"name": "콘크리트 타설",
|
||||
"unit": "㎥",
|
||||
"from_components": ["콘크리트", "버림콘크리트"],
|
||||
"kind_suffix": "concrete_placing"
|
||||
},
|
||||
{
|
||||
"code": "FP-12-38",
|
||||
"name": "유로폼",
|
||||
"unit": "㎡",
|
||||
"from_components": ["유로폼"],
|
||||
"kind_suffix": "euroform_type"
|
||||
},
|
||||
{
|
||||
"code": "FP-12-04",
|
||||
"name": "합판거푸집",
|
||||
"unit": "㎡",
|
||||
"from_components": ["합판거푸집"],
|
||||
"kind_suffix": "formwork_reuse"
|
||||
},
|
||||
{
|
||||
"code": "FP-12-03",
|
||||
"name": "철근 현장가공 및 조립",
|
||||
"unit": "ton",
|
||||
"from_components": ["이형철근 D13", "이형철근 D16"],
|
||||
"unit_from": "kg",
|
||||
"kind_suffix": "rebar_complexity",
|
||||
"why": "철근은 크기별 식이 없어 울진 기번3 구조도 2×2 관측값만 섬 — 다른 크기는 이 조각이 비어 묶음이 막힘(2026-09-15 브레인 판정 ②)"
|
||||
},
|
||||
{
|
||||
"code": "FP-12-20",
|
||||
"name": "강관동바리",
|
||||
"unit": "㎥",
|
||||
"from_components": ["동바리"]
|
||||
},
|
||||
{
|
||||
"code": "FP-12-19",
|
||||
"name": "강관비계",
|
||||
"unit": "㎡",
|
||||
"from_components": ["비계"]
|
||||
},
|
||||
{
|
||||
"code": "FP-12-25",
|
||||
"name": "기초잡석",
|
||||
"unit": "㎥",
|
||||
"from_components": ["기초잡석"],
|
||||
"why": "BOX 근거 둘(예제·울진)에는 잡석 줄이 없으나 **버림이 선 구조물엔 기초잡석이 함께 섬**(사용자 확정 3차 ② · 옹벽과 같은 규칙) — 원단위에 선 줄을 묶음에서 빼면 수량만 보이고 금액이 빠짐"
|
||||
}
|
||||
],
|
||||
"why": "품셈 12장에 「BOX암거」 공종이 없음 — 산림과임업기술(임도) 5장 수량산출 예제가 구체콘크리트·기초·거푸집·동바리·철근으로 가르고, 실무 울진 기번3 「암거수량집계표(2×2)」가 레미콘·버림·유로폼·합판·철근·동바리·비계로 셈(2026-09-15 브레인 12장 D).",
|
||||
"needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김"
|
||||
}
|
||||
],
|
||||
"unit_conversion": {
|
||||
|
||||
@@ -19,31 +19,29 @@ if str(ROOT) not in sys.path:
|
||||
|
||||
from B06_Section.B06_Section_Engine_Culvert import ( # noqa: E402
|
||||
BOX_COVER_M,
|
||||
FORD_SLAB_THICKNESS_M,
|
||||
FORD_WALL_THICKNESS_M,
|
||||
_box_set,
|
||||
attach_culvert_sets,
|
||||
)
|
||||
|
||||
|
||||
def test_box_set_defaults() -> None:
|
||||
"""저장 옵션이 없으면 레지스트리 기본 2.0×2.0에 세월교 두께를 얹는다."""
|
||||
"""옵션이 없으면 등록부 기본 2.0×2.0 · 두께도 등록부 제안값(2026-09-15 · 세월교 승계 걷음)."""
|
||||
spec = _box_set(None)
|
||||
assert spec["type"] == "box"
|
||||
assert spec["inner_width_m"] == 2.0
|
||||
assert spec["inner_height_m"] == 2.0
|
||||
assert spec["wall_thickness_m"] == FORD_WALL_THICKNESS_M
|
||||
assert spec["slab_thickness_m"] == FORD_SLAB_THICKNESS_M
|
||||
assert spec["top_thickness_m"] == FORD_SLAB_THICKNESS_M
|
||||
assert spec["wall_thickness_m"] == 0.3
|
||||
assert spec["slab_thickness_m"] == 0.25
|
||||
assert spec["top_thickness_m"] == 0.25
|
||||
assert spec["cover_m"] == BOX_COVER_M
|
||||
# 도로 방향 길이 = 내공 폭 + 측벽 두 장.
|
||||
assert spec["span_m"] == 2.0 + 2 * FORD_WALL_THICKNESS_M
|
||||
assert spec["span_m"] == 2.0 + 2 * 0.3
|
||||
|
||||
|
||||
def test_box_wing_extends_slab() -> None:
|
||||
"""저판 편측 연장 = 날개벽 길이 × cos(각도). 세월교와 같은 산식이다."""
|
||||
spec = _box_set({"body_width_m": 3, "body_height_m": 3, "wing_in_length_m": 2})
|
||||
assert spec["span_m"] == 3 + 2 * FORD_WALL_THICKNESS_M
|
||||
assert spec["span_m"] == 3 + 2 * 0.3
|
||||
assert spec["wing_in"]["slab_extend_m"] == round(2 * math.cos(math.radians(45)), 3)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""BOX암거 두께·연장 칸 — 등록부가 정본, B06 그림이 그 칸을 읽음 (2026-09-15 브레인 12장 D ①③).
|
||||
|
||||
① 제안값 = 산림과임업기술(임도) 5장 수량산출 예제 — 벽 0.3 · 판 0.25 · 헌치 0.2 · 버림 0.1
|
||||
근거에 KDS 44 90 00 4.5.2 「부재 최소두께 300㎜」(도로 기준)를 나란히 — 판 0.25 는 못 미침
|
||||
② 종전 B06 상수(벽 0.2 · 판 0.3 — 세월교 값 승계)는 근거 없는 값이라 걷음
|
||||
③ 연장 칸 — 기본값 없음
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from B06_Section.B06_Section_Engine_Culvert import _box_set
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _box_options() -> dict[str, dict]:
|
||||
registry = json.loads(
|
||||
(ROOT / "B05_Profile" / "B05_Profile_Structure_Types.json").read_text(encoding="utf-8")
|
||||
)
|
||||
types = registry if isinstance(registry, list) else registry.get("types") or []
|
||||
box = next(t for t in types if t["type_id"] == "box_culvert")
|
||||
return {option["key"]: option for option in box["options"]}
|
||||
|
||||
|
||||
def test_등록부에_두께_헌치_버림_연장_칸이_선다() -> None:
|
||||
options = _box_options()
|
||||
expected = {
|
||||
"wall_thickness_m": 0.3,
|
||||
"slab_thickness_m": 0.25,
|
||||
"haunch_m": 0.2,
|
||||
"blinding_thickness_m": 0.1,
|
||||
}
|
||||
for key, value in expected.items():
|
||||
assert options[key]["default"] == value, key
|
||||
assert "산림과임업기술" in options[key]["default_basis"], key
|
||||
# 판 0.25 는 KDS 도로 최소 300㎜ 에 못 미침 — 나란히 적어 설계자가 고르게.
|
||||
assert "KDS" in options["slab_thickness_m"]["default_basis"]
|
||||
assert "300" in options["slab_thickness_m"]["default_basis"]
|
||||
assert "KDS" in options["wall_thickness_m"]["default_basis"]
|
||||
# 연장은 기본값 없음.
|
||||
assert options["length_m"]["default"] is None
|
||||
|
||||
|
||||
def test_B06_세트가_등록부_칸을_읽는다() -> None:
|
||||
spec = _box_set(None)
|
||||
assert (spec["wall_thickness_m"], spec["slab_thickness_m"], spec["top_thickness_m"]) == (
|
||||
0.3,
|
||||
0.25,
|
||||
0.25,
|
||||
)
|
||||
assert spec["span_m"] == 2.0 + 2 * 0.3
|
||||
stored = _box_set({"wall_thickness_m": 0.35, "slab_thickness_m": 0.3})
|
||||
assert (stored["wall_thickness_m"], stored["slab_thickness_m"], stored["top_thickness_m"]) == (
|
||||
0.35,
|
||||
0.3,
|
||||
0.3,
|
||||
)
|
||||
assert stored["span_m"] == 2.0 + 2 * 0.35
|
||||
@@ -0,0 +1,102 @@
|
||||
"""BOX암거 전개식 — m당 × 연장 (2026-09-15 브레인 12장 D 판정 ①~⑤).
|
||||
|
||||
근거 둘(구조 같음)
|
||||
산림과임업기술(임도) 5장 「구조물도에 의한 수량산출(예)」 — 구체 = (외곽 − 유수구 + 헌치) ·
|
||||
기초 폭×두께 · 거푸집 외벽·유수구·받침판·귀면 · 동바리 유수구 단면
|
||||
실무 울진 기번3 구조도 「암거수량집계표(2×2)」 m당 — 레미콘 2.84 · 버림 0.28 · 유로폼 11.132 ·
|
||||
합판 0.2 · 철근 0.336t(H13 0.179 · H16 0.157) · 동바리 3.92 · 비계 벽체 5.2 + 시종점 13.52
|
||||
⚠ 철근은 크기별 식이 없어 **2×2 관측값만** · 날개벽·토공은 사유.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff
|
||||
from B08_Quantity.B08_Quantity_Engine_Pipe import facility_structures
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table, expand
|
||||
|
||||
ULJIN_2X2 = {
|
||||
"body_width_m": 2.0,
|
||||
"body_height_m": 2.0,
|
||||
"wall_thickness_m": 0.3,
|
||||
"slab_thickness_m": 0.3,
|
||||
"haunch_m": 0.2,
|
||||
"blinding_thickness_m": 0.1,
|
||||
"length_m": 10.0,
|
||||
}
|
||||
|
||||
|
||||
def _amounts(options: dict) -> tuple[dict, list[str], object]:
|
||||
result = expand({"type_id": "box_culvert", "options": options}, {"box_culvert": "BOX암거"})
|
||||
return (
|
||||
{(c.name, c.spec): c.amount for c in result.components},
|
||||
result.notes,
|
||||
result,
|
||||
)
|
||||
|
||||
|
||||
def test_울진_2x2_m당_값을_연장에_곱한다() -> None:
|
||||
got, notes, result = _amounts(ULJIN_2X2)
|
||||
assert got[("콘크리트", "")] == pytest.approx(2.84 * 10)
|
||||
assert got[("버림콘크리트", "")] == pytest.approx(0.28 * 10)
|
||||
assert got[("유로폼", "")] == pytest.approx(
|
||||
11.1314 * 10, abs=0.01
|
||||
) # 원문 11.132(귀면 0.566 반올림)
|
||||
assert got[("합판거푸집", "")] == pytest.approx(0.2 * 10)
|
||||
assert got[("이형철근", "D13")] == pytest.approx(179 * 10)
|
||||
assert got[("이형철근", "D16")] == pytest.approx(157 * 10)
|
||||
assert got[("동바리", "")] == pytest.approx(3.92 * 10)
|
||||
# 비계 — 벽체 m당 5.2 × 연장 + 시종점 두 면 13.52 는 한 번(원문은 m당에 넣었음 · 사유).
|
||||
assert got[("비계", "")] == pytest.approx(5.2 * 10 + 13.52)
|
||||
assert (result.billing_unit, result.billing_quantity) == ("m", 10.0)
|
||||
text = " ".join(notes)
|
||||
assert "관측" in text and "날개벽" in text
|
||||
|
||||
|
||||
def test_예제_두께_기본값으로_구체가_줄어든다() -> None:
|
||||
got, _notes, _r = _amounts({**ULJIN_2X2, "slab_thickness_m": 0.25})
|
||||
# (2.6 × 2.5 − 2 × 2 + 0.2² ÷ 2 × 4) = 2.58㎥/m
|
||||
assert got[("콘크리트", "")] == pytest.approx(2.58 * 10)
|
||||
|
||||
|
||||
def test_2x2_가_아니면_철근은_사유() -> None:
|
||||
got, notes, _r = _amounts({**ULJIN_2X2, "body_width_m": 3.0})
|
||||
assert not [key for key in got if key[0] == "이형철근"]
|
||||
assert any("크기별 식 없음" in note and "2×2" in note for note in notes)
|
||||
assert got[("콘크리트", "")] > 0 # 나머지는 섬
|
||||
|
||||
|
||||
def test_연장이_없으면_안_선다() -> None:
|
||||
got, notes, _r = _amounts({k: v for k, v in ULJIN_2X2.items() if k != "length_m"})
|
||||
assert got == {}
|
||||
assert any("연장" in note for note in notes)
|
||||
|
||||
|
||||
def test_치수를_안_적으면_기본값으로_서되_미확정() -> None:
|
||||
rows = facility_structures(
|
||||
[{"chainage_m": 50.0, "facility": "box_culvert", "options": {"length_m": 8.0}}]
|
||||
)
|
||||
box = next(row for row in rows if row["type_id"] == "box_culvert")
|
||||
assert box["unconfirmed"] and "측벽 두께" in box["unconfirmed"]
|
||||
assert box["options"]["wall_thickness_m"] == 0.3 and box["options"]["slab_thickness_m"] == 0.25
|
||||
stored = facility_structures(
|
||||
[{"chainage_m": 50.0, "facility": "box_culvert", "options": ULJIN_2X2}]
|
||||
)
|
||||
assert not next(r for r in stored if r["type_id"] == "box_culvert").get("unconfirmed")
|
||||
|
||||
|
||||
def test_인계_묶음이_조각을_세운다() -> None:
|
||||
table = build_table(
|
||||
facility_structures(
|
||||
[{"chainage_m": 50.0, "facility": "box_culvert", "options": ULJIN_2X2}]
|
||||
),
|
||||
{"box_culvert": "BOX암거"},
|
||||
)
|
||||
row = next(
|
||||
r for r in build_handoff(unit_quantity_table=table)["work_items"] if "BOX" in r["name"]
|
||||
)
|
||||
parts = {part["code"].split("#")[0]: part for part in row["composite_parts"] or []}
|
||||
assert {"FP-12-01-01", "FP-12-04", "FP-12-38", "FP-12-03", "FP-12-20", "FP-12-19"} <= set(parts)
|
||||
assert parts["FP-12-03"]["quantity"] == pytest.approx((179 + 157) * 10 / 1000)
|
||||
assert parts["FP-12-20"]["quantity"] == pytest.approx(3.92 * 10)
|
||||
@@ -116,7 +116,8 @@ def test_독립_기슭막이_높이는_설계자_입력_없으면_미확정과_
|
||||
|
||||
def test_산출식_없는_세월교는_사유로_서고_터파기_빠짐_줄을_안_만든다() -> None:
|
||||
table = _table([{"facility": "ford_bridge", "chainage_m": 30.0, "options": {}}])
|
||||
assert "산출식이 아직 없습니다" in table["structures"][0]["notes"][0]
|
||||
# 2026-09-15 브레인 판정 ⑥ — 「아직 없음」이 아니라 「근거 없음」으로 닫음.
|
||||
assert "세월교 수량 근거 없음" in table["structures"][0]["notes"][0]
|
||||
rows = build_handoff(unit_quantity_table=table)["work_items"]
|
||||
assert not any("터파기가 안 선 구조물" in str(row.get("spec_detail")) for row in rows)
|
||||
|
||||
|
||||
@@ -114,8 +114,22 @@ def test_동바리는_대상이_없으면_없다고_말할것() -> None:
|
||||
status = shoring_status()
|
||||
assert status["applicable"] is False
|
||||
assert status["reason"]
|
||||
assert set(status["pending_types"]) == {"box_culvert", "ford_bridge"}
|
||||
# BOX암거는 2026-09-15 전개식이 서서 대상에서 빠짐 — 세월교만 남음(근거 없음 사유).
|
||||
assert set(status["pending_types"]) == {"ford_bridge"}
|
||||
|
||||
|
||||
def test_표에도_동바리_상태가_실림() -> None:
|
||||
assert build_table([옹벽()])["shoring"]["applicable"] is False
|
||||
box = {
|
||||
"type_id": "box_culvert",
|
||||
"options": {
|
||||
"body_width_m": 2.0,
|
||||
"body_height_m": 2.0,
|
||||
"wall_thickness_m": 0.3,
|
||||
"slab_thickness_m": 0.3,
|
||||
"haunch_m": 0.2,
|
||||
"blinding_thickness_m": 0.1,
|
||||
"length_m": 5.0,
|
||||
},
|
||||
}
|
||||
assert build_table([box])["shoring"]["applicable"] is True
|
||||
|
||||
@@ -169,13 +169,16 @@ def test_숫자와_글자_규격을_같이_본다() -> None:
|
||||
assert table.find("retaining_wall", {"form": "반중력식", "height_m": 1.9999}) is None
|
||||
|
||||
|
||||
def test_BOX암거는_미확보로_남음() -> None:
|
||||
"""⚠ 두께가 저장돼 있지 않아 식도 못 세우고 관측값도 없다 — 지어내면 콘크리트·거푸집·
|
||||
철근으로 번져 나간다. 사용자 확정 대기."""
|
||||
def test_BOX암거는_관측표가_아니라_전개식으로_섬() -> None:
|
||||
"""2026-09-15 — 두께 칸이 등록부에 서고 전개식(`_UnitQuantity_Box`)이 생겨 관측표 몫이 아님.
|
||||
|
||||
관측값은 여전히 없음(울진 2×2 철근만 전개식 안에 관측값으로 듦) · 세월교는 사유로 닫힘.
|
||||
"""
|
||||
table = load_observed_table()
|
||||
assert table.find("box_culvert", {"body_width_m": 3.0, "body_height_m": 1.2}) is None
|
||||
reasons = [item["type_id"] for item in table.not_found["items"]]
|
||||
assert "box_culvert" in reasons
|
||||
reasons = {item["type_id"]: item for item in table.not_found["items"]}
|
||||
assert "box_culvert" not in reasons
|
||||
assert "세월교 수량 근거 없음" in reasons["ford_bridge"]["why"]
|
||||
|
||||
|
||||
def test_파일이_없으면_전부_미확보() -> None:
|
||||
|
||||
Reference in New Issue
Block a user