Merge remote-tracking branch 'origin/sub_desktop_1' into main_laptop_1
This commit is contained in:
@@ -18,6 +18,12 @@
|
||||
계수의 유일한 정의처는 `config.config_system_design.EARTHWORK_CONVERSION_FACTORS` 이며
|
||||
여기서 값을 다시 적지 않는다.
|
||||
|
||||
측구터파기 토사·암 — 설계가 가른 값을 그대로 읽는다
|
||||
B06 이 지반 유형 + 암반 경계선으로 이미 갈라 냈다(`ditch_soil_area_m2`·
|
||||
`ditch_rock_area_m2` · 사유 `ditch_split_basis`). 여기서 다시 나누지 않는다 —
|
||||
나누면 같은 측구가 횡단도와 토적표에서 다른 숫자로 선다. 가름이 붙기 전 저장분만
|
||||
절토 면적비 안분으로 떨어지고, 그 줄에는 사유가 남는다(`_split_ditch`).
|
||||
|
||||
⚠ 숫자는 자르지 않는다 (PLAN 8-16)
|
||||
품셈 1-2-2 의 소수 자리는 **표기 규칙**이다. 계산은 전정밀로 두고 화면·출력에서만
|
||||
반올림한다. 원가 쪽(줄마다 원 단위 절사)과 규칙이 반대이므로 섞지 말 것.
|
||||
@@ -34,6 +40,14 @@ from config.config_system_design import EARTHWORK_CONVERSION_FACTORS
|
||||
# 값이 없으면 리핑암으로 본다(발파암보다 보수적으로 적은 쪽).
|
||||
_DEFAULT_ROCK_KIND = "ripping_rock"
|
||||
|
||||
# 측구 가름 근거 — 설계가 가른 값이 없어 절토 면적비로 안분했을 때만 붙는다.
|
||||
# B06 가 내는 네 갈래(`no_ditch`·`soil_ground`·`rock_boundary`·`rock_ground_no_boundary`)와
|
||||
# 섞이지 않게 이름을 따로 둔다. 이 값이 표에 보이면 **설계가 가른 것이 아니다.**
|
||||
_FALLBACK_BASIS = "cut_area_ratio_fallback"
|
||||
|
||||
#: 폴백을 탄 측점 줄에 남기는 사유. 숫자만 봐서는 안분인지 설계값인지 알 수 없다.
|
||||
_FALLBACK_NOTE = "측구 가름값이 설계에 없어 절토 토사:암 면적비로 안분함"
|
||||
|
||||
|
||||
def _factor(kind: str) -> float:
|
||||
"""지반유형 → 다짐 환산계수. 모르는 유형이면 토사로 본다."""
|
||||
@@ -50,6 +64,11 @@ class StationArea:
|
||||
cut_rock_area_m2: float = 0.0
|
||||
fill_area_m2: float = 0.0
|
||||
ditch_area_m2: float = 0.0
|
||||
# 측구 가름은 **설계가 낸 값**이다(B06 `ditch_soil_area_m2`·`ditch_rock_area_m2`).
|
||||
# `None` 은 「설계가 안 냈다」는 뜻이고 0.0 과 다르다 — 0.0 은 설계가 낸 「없음」이다.
|
||||
ditch_soil_area_m2: float | None = None
|
||||
ditch_rock_area_m2: float | None = None
|
||||
ditch_split_basis: str | None = None
|
||||
cut_rock_kind: str | None = None
|
||||
|
||||
@classmethod
|
||||
@@ -58,12 +77,19 @@ class StationArea:
|
||||
value = design.get(key)
|
||||
return float(value) if isinstance(value, (int, float)) else 0.0
|
||||
|
||||
def optional(key: str) -> float | None:
|
||||
value = design.get(key)
|
||||
return float(value) if isinstance(value, (int, float)) else None
|
||||
|
||||
return cls(
|
||||
chainage_m=float(chainage_m),
|
||||
cut_soil_area_m2=num("cut_soil_area_m2"),
|
||||
cut_rock_area_m2=num("cut_rock_area_m2"),
|
||||
fill_area_m2=num("fill_area_m2"),
|
||||
ditch_area_m2=num("ditch_area_m2"),
|
||||
ditch_soil_area_m2=optional("ditch_soil_area_m2"),
|
||||
ditch_rock_area_m2=optional("ditch_rock_area_m2"),
|
||||
ditch_split_basis=design.get("ditch_split_basis") or None,
|
||||
cut_rock_kind=design.get("cut_rock_kind") or None,
|
||||
)
|
||||
|
||||
@@ -92,25 +118,41 @@ class EarthworkRow:
|
||||
diverted_m3: float = 0.0
|
||||
balance_m3: float = 0.0
|
||||
cumulative_m3: float = 0.0
|
||||
# 측구를 무슨 근거로 갈랐나 — 설계가 낸 사유를 그대로 싣는다(빈 문자열은 사유 없음).
|
||||
ditch_split_basis: str = ""
|
||||
notes: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def _split_ditch(area: StationArea) -> tuple[float, float]:
|
||||
"""측구터파기 단면적을 토사·암으로 가른다.
|
||||
def _split_ditch(area: StationArea) -> tuple[float, float, str]:
|
||||
"""측구터파기 단면적을 토사·암으로 가른다 — **설계가 가른 값을 그대로 읽는다.**
|
||||
|
||||
⚠ TODO(미결 · PLAN 8-4b) — 설계가 측구를 토사·암으로 나눠 주지 않는다(`ditch_area_m2`
|
||||
한 값뿐). 실무 토적표는 둘로 갈라 적으므로, **그 측점의 절토 토사:암 면적비로 안분**한다.
|
||||
측구는 절토부에 파므로 같은 지반을 만난다는 것이 근거다. 설계가 측구 지반을 따로 내주게
|
||||
되면 이 함수만 갈아끼운다.
|
||||
가름의 주인은 횡단 설계다. B06 이 절토 분리와 같은 근거(지반 유형 + 암반 경계선)로
|
||||
`ditch_soil_area_m2`·`ditch_rock_area_m2` 를 내고 사유를 `ditch_split_basis` 로
|
||||
함께 낸다. 토적표가 여기서 다시 나누면 **같은 측구가 횡단도와 토적표에서 다른 숫자로
|
||||
선다** — 그 어긋남을 없애는 자리다.
|
||||
|
||||
못 가른 측점(암 지반인데 암반 경계선이 없음)은 설계가 **전량 암 + 사유**로 내므로
|
||||
여기서 손보지 않는다.
|
||||
|
||||
⚠ 폴백은 **설계값이 아예 없을 때만** — 가름이 붙기 전 저장분이다. 그때만 그 측점의
|
||||
절토 토사:암 면적비로 안분하고, 근거를 `cut_area_ratio_fallback` 으로 남겨
|
||||
「설계가 가른 것이 아니다」가 표에 드러나게 한다.
|
||||
"""
|
||||
if area.ditch_soil_area_m2 is not None or area.ditch_rock_area_m2 is not None:
|
||||
return (
|
||||
float(area.ditch_soil_area_m2 or 0.0),
|
||||
float(area.ditch_rock_area_m2 or 0.0),
|
||||
area.ditch_split_basis or "",
|
||||
)
|
||||
|
||||
ditch = area.ditch_area_m2
|
||||
if ditch <= 0:
|
||||
return 0.0, 0.0
|
||||
return 0.0, 0.0, "no_ditch"
|
||||
soil, rock = area.cut_soil_area_m2, area.cut_rock_area_m2
|
||||
total = soil + rock
|
||||
if total <= 0:
|
||||
return ditch, 0.0 # 절토가 없으면 토사로 본다.
|
||||
return ditch * soil / total, ditch * rock / total
|
||||
return ditch, 0.0, _FALLBACK_BASIS # 절토가 없으면 토사로 본다.
|
||||
return ditch * soil / total, ditch * rock / total, _FALLBACK_BASIS
|
||||
|
||||
|
||||
def build_rows(stations: Iterable[StationArea]) -> list[EarthworkRow]:
|
||||
@@ -122,7 +164,7 @@ def build_rows(stations: Iterable[StationArea]) -> list[EarthworkRow]:
|
||||
cumulative = 0.0
|
||||
|
||||
for station in ordered:
|
||||
ditch_soil, ditch_rock = _split_ditch(station)
|
||||
ditch_soil, ditch_rock, ditch_basis = _split_ditch(station)
|
||||
soil_factor = _factor("soil")
|
||||
rock_factor = _factor(station.cut_rock_kind or _DEFAULT_ROCK_KIND)
|
||||
row = EarthworkRow(
|
||||
@@ -132,7 +174,10 @@ def build_rows(stations: Iterable[StationArea]) -> list[EarthworkRow]:
|
||||
ditch_soil_area_m2=ditch_soil,
|
||||
ditch_rock_area_m2=ditch_rock,
|
||||
fill_area_m2=station.fill_area_m2,
|
||||
ditch_split_basis=ditch_basis,
|
||||
)
|
||||
if ditch_basis == _FALLBACK_BASIS:
|
||||
row.notes.append(_FALLBACK_NOTE)
|
||||
if previous is not None:
|
||||
distance = station.chainage_m - previous.chainage_m
|
||||
row.distance_m = distance
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
값은 어디서 오나
|
||||
측점별 단면적은 **B06 이 이미 낸 정본**이다(`cross_sections.data.design` 의
|
||||
`cut_soil_area_m2`·`cut_rock_area_m2`·`fill_area_m2`·`ditch_area_m2`).
|
||||
`cut_soil_area_m2`·`cut_rock_area_m2`·`fill_area_m2`·`ditch_area_m2`, 그리고
|
||||
측구 가름값 `ditch_soil_area_m2`·`ditch_rock_area_m2`·`ditch_split_basis`).
|
||||
설계 dict 를 통째로 `StationArea.from_design` 에 넘기므로 키가 늘어도 여기는 안 고친다.
|
||||
B08 은 그것을 다시 재지 않고 **평균단면적법으로 체적화만** 한다.
|
||||
|
||||
계산 자리 (CLAUDE.md 5장)
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
* 실무 산출서와 눈으로 대조를 못 한다. 열 순서·머리글 문구를 실무 시트에 맞춘다.
|
||||
*
|
||||
* ⚠ 소수 자리는 표기 규칙일 뿐이다 (PLAN 8-16)
|
||||
* 서버는 전정밀 값을 준다. 자르는 것은 여기(화면)뿐이다. 실무 시트 관측 그대로
|
||||
* 단면적·체적 2자리 · 보정량계·유용토·차인·누가 1자리 · 거리 정수로 보인다.
|
||||
* 서버는 전정밀 값을 준다. 자르는 것은 여기(화면)뿐이다. 자리수는 **산림사업 표준품셈**을
|
||||
* 따른다 — 단면적 ㎡ 1자리 · 입적·보정량·보정량계·유용토·차인·누가 ㎥ 2자리 ·
|
||||
* 체적 합계 정수 · 거리 정수. 실무 시트 관측값(단면적 2자리 등)이 아니라 품셈이 기준이다.
|
||||
* ⚠ 매 단계 반올림은 오차가 쌓이므로 하지 않는다 — 자르는 곳은 이 표 한 곳뿐이다.
|
||||
* 원가 쪽(줄마다 원 단위 절사)과 규칙이 반대이므로 그 코드를 여기로 옮기지 말 것.
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -34,6 +36,10 @@ export interface EarthworkRow {
|
||||
diverted_m3: number;
|
||||
balance_m3: number;
|
||||
cumulative_m3: number;
|
||||
/** 측구를 무슨 근거로 갈랐나 — B06 사유 그대로. `cut_area_ratio_fallback` 이면 안분이다. */
|
||||
ditch_split_basis?: string;
|
||||
/** 줄에 남은 사유(폴백 안분 등). 표 칸이 아니라 주기로 보일 값이다. */
|
||||
notes?: string[];
|
||||
}
|
||||
|
||||
/** 사면 4계열 — 계열별 (거리, 면적). 키는 `면고르기_성토면` 식으로 엔진과 같다. */
|
||||
@@ -105,9 +111,14 @@ export interface EarthworkTable {
|
||||
settings?: QuantitySettings;
|
||||
}
|
||||
|
||||
/** 표 칸에 들어갈 수 있는 열 — 숫자 칸만 고른다(사유·주기는 표 밖이다). */
|
||||
type NumericColumnKey = {
|
||||
[K in keyof EarthworkRow]-?: NonNullable<EarthworkRow[K]> extends number ? K : never;
|
||||
}[keyof EarthworkRow];
|
||||
|
||||
/** 열 하나. `digits` 는 **표기 자리**이며 값 자체는 자르지 않는다. */
|
||||
interface Column {
|
||||
key: keyof EarthworkRow;
|
||||
key: NumericColumnKey;
|
||||
digits: number;
|
||||
/** 합계행에 낼지 — 단면적은 합이 뜻이 없어 비운다(실무 시트도 비어 있다). */
|
||||
sum?: boolean;
|
||||
@@ -123,7 +134,7 @@ const GROUPS: { label: string; sub: { label: string; cols: Column[] }[] }[] = [
|
||||
{
|
||||
label: "토 사",
|
||||
cols: [
|
||||
{ key: "cut_soil_area_m2", digits: 2 },
|
||||
{ key: "cut_soil_area_m2", digits: 1 },
|
||||
{ key: "cut_soil_volume_m3", digits: 2, sum: true },
|
||||
{ key: "cut_soil_adjusted_m3", digits: 2, sum: true },
|
||||
],
|
||||
@@ -131,7 +142,7 @@ const GROUPS: { label: string; sub: { label: string; cols: Column[] }[] }[] = [
|
||||
{
|
||||
label: "암 석",
|
||||
cols: [
|
||||
{ key: "cut_rock_area_m2", digits: 2 },
|
||||
{ key: "cut_rock_area_m2", digits: 1 },
|
||||
{ key: "cut_rock_volume_m3", digits: 2, sum: true },
|
||||
{ key: "cut_rock_adjusted_m3", digits: 2, sum: true },
|
||||
],
|
||||
@@ -144,7 +155,7 @@ const GROUPS: { label: string; sub: { label: string; cols: Column[] }[] }[] = [
|
||||
{
|
||||
label: "토 사",
|
||||
cols: [
|
||||
{ key: "ditch_soil_area_m2", digits: 2 },
|
||||
{ key: "ditch_soil_area_m2", digits: 1 },
|
||||
{ key: "ditch_soil_volume_m3", digits: 2, sum: true },
|
||||
{ key: "ditch_soil_adjusted_m3", digits: 2, sum: true },
|
||||
],
|
||||
@@ -152,7 +163,7 @@ const GROUPS: { label: string; sub: { label: string; cols: Column[] }[] }[] = [
|
||||
{
|
||||
label: "암 석",
|
||||
cols: [
|
||||
{ key: "ditch_rock_area_m2", digits: 2 },
|
||||
{ key: "ditch_rock_area_m2", digits: 1 },
|
||||
{ key: "ditch_rock_volume_m3", digits: 2, sum: true },
|
||||
{ key: "ditch_rock_adjusted_m3", digits: 2, sum: true },
|
||||
],
|
||||
@@ -161,7 +172,7 @@ const GROUPS: { label: string; sub: { label: string; cols: Column[] }[] }[] = [
|
||||
},
|
||||
{
|
||||
label: "",
|
||||
sub: [{ label: "보정량계", cols: [{ key: "adjusted_total_m3", digits: 1, sum: true }] }],
|
||||
sub: [{ label: "보정량계", cols: [{ key: "adjusted_total_m3", digits: 2, sum: true }] }],
|
||||
},
|
||||
{
|
||||
label: "성 토",
|
||||
@@ -169,15 +180,15 @@ const GROUPS: { label: string; sub: { label: string; cols: Column[] }[] }[] = [
|
||||
{
|
||||
label: "",
|
||||
cols: [
|
||||
{ key: "fill_area_m2", digits: 2 },
|
||||
{ key: "fill_area_m2", digits: 1 },
|
||||
{ key: "fill_volume_m3", digits: 2, sum: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ label: "", sub: [{ label: "유 용 토", cols: [{ key: "diverted_m3", digits: 1, sum: true }] }] },
|
||||
{ label: "", sub: [{ label: "차인토량", cols: [{ key: "balance_m3", digits: 1, sum: true }] }] },
|
||||
{ label: "", sub: [{ label: "누가토량", cols: [{ key: "cumulative_m3", digits: 1 }] }] },
|
||||
{ label: "", sub: [{ label: "유 용 토", cols: [{ key: "diverted_m3", digits: 2, sum: true }] }] },
|
||||
{ label: "", sub: [{ label: "차인토량", cols: [{ key: "balance_m3", digits: 2, sum: true }] }] },
|
||||
{ label: "", sub: [{ label: "누가토량", cols: [{ key: "cumulative_m3", digits: 2 }] }] },
|
||||
];
|
||||
|
||||
/** 사면 4계열 — 실무 토적표 오른쪽 절반(V~AI). 계열마다 (거리, 면적) 쌍이다.
|
||||
@@ -210,6 +221,9 @@ const SLOPE_GROUPS: { label: string; faces: { key: string; label: string }[] }[]
|
||||
/** 사면 계열의 소분류 머리글 — 거리(사면길이)와 면적 두 칸. */
|
||||
const SLOPE_LABELS = ["거 리", "면 적"];
|
||||
|
||||
/** 체적 합계 자리수 — 품셈이 정수로 정해 둔다. 거리 합계(m)도 같은 정수다. */
|
||||
const SUM_DIGITS = 0;
|
||||
|
||||
/** 소분류가 「단면적·입적·보정량」 삼중조일 때 붙는 3단 머리글 문구. */
|
||||
const TRIPLE_LABELS = ["단면적", "입 적", "보정량"];
|
||||
const PAIR_LABELS = ["단면적", "입 적"];
|
||||
@@ -333,7 +347,9 @@ function buildFoot(totals: Record<string, number>, slope?: SlopeTable): HTMLTabl
|
||||
flatColumns().forEach((column, index) => {
|
||||
const td = document.createElement("td");
|
||||
if (index === 0) td.textContent = "계";
|
||||
else if (column.sum) td.textContent = cell(totals[column.key], column.digits);
|
||||
// 합계는 **정수**다(품셈 자리수 규칙). 열마다 다른 자리수를 그대로 쓰지 않는다 —
|
||||
// 합계행에 남는 소수는 실무 산출서와 모양이 어긋나는 자리다. 값은 안 자른다.
|
||||
else if (column.sum) td.textContent = cell(totals[column.key], SUM_DIGITS);
|
||||
tr.append(td);
|
||||
});
|
||||
// 사면은 면적만 합한다 — 거리(사면길이)는 합이 뜻이 없다.
|
||||
|
||||
@@ -115,8 +115,55 @@ def test_암은_토사와_다른_계수() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_측구_안분은_절토_토사암_비율() -> None:
|
||||
"""측구 지반이 따로 안 오므로 그 측점 절토 비율로 가른다(엔진 주석의 TODO 자리)."""
|
||||
def test_설계가_가른_값을_그대로_읽음() -> None:
|
||||
"""측구 가름의 주인은 횡단 설계다 — 토적표가 절토 비율로 다시 나누면 안 된다.
|
||||
|
||||
절토 비율(3:1)로 안분하면 0.30/0.10 이 되지만, 설계는 암반 경계선으로 0.05/0.35 를
|
||||
냈다. 그 값이 그대로 서야 횡단도와 토적표의 숫자가 같아진다.
|
||||
"""
|
||||
stations = [
|
||||
StationArea(chainage_m=0.0),
|
||||
StationArea(
|
||||
chainage_m=10.0,
|
||||
cut_soil_area_m2=3.0,
|
||||
cut_rock_area_m2=1.0,
|
||||
ditch_area_m2=0.4,
|
||||
ditch_soil_area_m2=0.05,
|
||||
ditch_rock_area_m2=0.35,
|
||||
ditch_split_basis="rock_boundary",
|
||||
cut_rock_kind="ripping_rock",
|
||||
),
|
||||
]
|
||||
row = build_rows(stations)[1]
|
||||
assert row.ditch_soil_area_m2 == pytest.approx(0.05)
|
||||
assert row.ditch_rock_area_m2 == pytest.approx(0.35)
|
||||
assert row.ditch_split_basis == "rock_boundary"
|
||||
assert row.notes == [] # 설계값을 읽었으므로 폴백 사유가 없다.
|
||||
|
||||
|
||||
def test_못_가른_측점은_사유와_함께_전량_암() -> None:
|
||||
"""암 지반인데 암반 경계선이 없으면 설계가 전량 암으로 낸다 — 여기서 손보지 않는다."""
|
||||
stations = [
|
||||
StationArea(chainage_m=0.0),
|
||||
StationArea(
|
||||
chainage_m=10.0,
|
||||
cut_soil_area_m2=2.0,
|
||||
cut_rock_area_m2=2.0,
|
||||
ditch_area_m2=0.18,
|
||||
ditch_soil_area_m2=0.0,
|
||||
ditch_rock_area_m2=0.18,
|
||||
ditch_split_basis="rock_ground_no_boundary",
|
||||
cut_rock_kind="blasting_rock",
|
||||
),
|
||||
]
|
||||
row = build_rows(stations)[1]
|
||||
assert row.ditch_soil_area_m2 == 0.0 # 절반씩 임의로 나누지 않는다.
|
||||
assert row.ditch_rock_area_m2 == pytest.approx(0.18)
|
||||
assert row.ditch_split_basis == "rock_ground_no_boundary"
|
||||
|
||||
|
||||
def test_설계값이_없을_때만_절토_비율로_안분() -> None:
|
||||
"""가름이 붙기 전 저장분 — 폴백으로 안분하되 사유를 남겨 설계값과 구분한다."""
|
||||
stations = [
|
||||
StationArea(chainage_m=0.0),
|
||||
StationArea(
|
||||
@@ -130,6 +177,8 @@ def test_측구_안분은_절토_토사암_비율() -> None:
|
||||
row = build_rows(stations)[1]
|
||||
assert row.ditch_soil_area_m2 == pytest.approx(0.3)
|
||||
assert row.ditch_rock_area_m2 == pytest.approx(0.1)
|
||||
assert row.ditch_split_basis == "cut_area_ratio_fallback"
|
||||
assert row.notes and "안분" in row.notes[0]
|
||||
|
||||
|
||||
def test_값을_자르지_않을것() -> None:
|
||||
@@ -145,9 +194,7 @@ def test_합계행() -> None:
|
||||
rows = build_rows(거창_앞_네_측점())
|
||||
total = totals(rows)
|
||||
assert total["distance_m"] == pytest.approx(30.0)
|
||||
assert total["cut_soil_volume_m3"] == pytest.approx(
|
||||
sum(r.cut_soil_volume_m3 for r in rows)
|
||||
)
|
||||
assert total["cut_soil_volume_m3"] == pytest.approx(sum(r.cut_soil_volume_m3 for r in rows))
|
||||
|
||||
|
||||
def test_표_모양() -> None:
|
||||
@@ -166,12 +213,26 @@ def test_설계결과에서_담기() -> None:
|
||||
"cut_rock_area_m2": 0.5,
|
||||
"fill_area_m2": 2.0,
|
||||
"ditch_area_m2": 0.18,
|
||||
"ditch_soil_area_m2": 0.12,
|
||||
"ditch_rock_area_m2": 0.06,
|
||||
"ditch_split_basis": "rock_boundary",
|
||||
"cut_rock_kind": "blasting_rock",
|
||||
}
|
||||
area = StationArea.from_design(20.0, design)
|
||||
assert area.chainage_m == 20.0
|
||||
assert area.cut_soil_area_m2 == 1.5
|
||||
assert area.cut_rock_kind == "blasting_rock"
|
||||
assert area.ditch_soil_area_m2 == 0.12
|
||||
assert area.ditch_rock_area_m2 == 0.06
|
||||
assert area.ditch_split_basis == "rock_boundary"
|
||||
|
||||
|
||||
def test_가름이_없는_저장분은_None으로_받음() -> None:
|
||||
"""0.0 과 「설계가 안 냄」은 다르다 — None 이라야 폴백이 선다."""
|
||||
area = StationArea.from_design(20.0, {"cut_soil_area_m2": 1.5, "ditch_area_m2": 0.18})
|
||||
assert area.ditch_soil_area_m2 is None
|
||||
assert area.ditch_rock_area_m2 is None
|
||||
assert area.ditch_split_basis is None
|
||||
|
||||
|
||||
def test_측점_순서가_뒤죽박죽이어도_이정순으로() -> None:
|
||||
|
||||
Reference in New Issue
Block a user