Merge remote-tracking branch 'origin/dev' into sub_laptop_5
충돌 = UI_Page.ts 메뉴 블록 옆 주석(596-4) — 둘 다 살림
This commit is contained in:
@@ -22,7 +22,7 @@ def test_thumb_line_fits_unit_box_and_caps_points():
|
||||
|
||||
def test_stage_names_follow_step_bar():
|
||||
assert len(summary.stage_names("ROAD")) == 7
|
||||
assert summary.stage_names("PLAN")[0] == "노선 분석" # 591-2 기준 지정 걷음
|
||||
assert summary.stage_names("PLAN")[0] == "예정노선" # 591-2 기준 지정 걷음
|
||||
assert summary.stage_names("EROSION") == summary.stage_names("ROAD")
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ def test_summaries_in_batch(monkeypatch, tmp_path):
|
||||
assert a["stale_days"] == 20 and a["owner_names"] == ["엄", "박"]
|
||||
assert a["metrics"] == {"length_m": 15.0, "stations": 3, "structures": 2}
|
||||
assert len(a["thumb"]) == 3 and a["done"] is False
|
||||
assert b["thumb"] is None and b["metrics"] == {} and b["stage_name"] == "노선 분석"
|
||||
assert b["thumb"] is None and b["metrics"] == {} and b["stage_name"] == "예정노선"
|
||||
assert db["queries"] == 5
|
||||
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ DRAW_READS = [
|
||||
("B05_Profile/B05_Profile_UI_RouteEdit_Recommend_Map.ts", '"--draw-text-secondary"'),
|
||||
("B05_Profile/B05_Profile_UI_Viewer.ts", '"--draw-surface-raised"'),
|
||||
("B04_PreProcess/B04_PreProcess_UI_Camera.ts", '"--draw-surface-raised"'),
|
||||
("D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts", '"--draw-primary"'),
|
||||
("D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts", '"--draw-map-sketch"'),
|
||||
("B04_PreProcess/B04_PreProcess_UI_MapOverlays.ts", '"--draw-warning"'),
|
||||
("B04_PreProcess/B04_PreProcess_UI_MapOverlays.ts", '"--draw-canvas"'),
|
||||
("B07_DesignDetail/openwebcad/src/App.css", "--cad-canvas: var(--draw-canvas);"),
|
||||
@@ -161,3 +161,25 @@ def test_changing_screen_tokens_keeps_drawing_colors():
|
||||
a = resolve(value, before[mode])
|
||||
assert "var(" not in a, (mode, rel, value)
|
||||
assert a == resolve(value, after[mode]), (mode, rel, value)
|
||||
|
||||
|
||||
def _lum(hex_color: str) -> float:
|
||||
rgb = [int(hex_color[i : i + 2], 16) / 255 for i in (1, 3, 5)]
|
||||
lin = [c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4 for c in rgb]
|
||||
return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2]
|
||||
|
||||
|
||||
def test_map_drawing_colors_fixed_and_readable_on_dark_map():
|
||||
"""600-1 지도 위 그림 색 — 테마 안 따름(어두움 블록에 없음) · 어두운 바탕 `#1f2937` · 검은 테 둘 다 ≥ 3 : 1."""
|
||||
blocks = _blocks(THEME.read_text(encoding="utf-8"))
|
||||
names = [
|
||||
k for k in blocks["light"] if re.fullmatch(r"--draw-map-(sketch|measure|warn|route-\d)", k)
|
||||
]
|
||||
assert len(names) == 8, names
|
||||
ground = _lum("#1f2937")
|
||||
for name in names:
|
||||
for mode in ("dark", "auto"):
|
||||
assert name not in blocks[mode], (mode, name)
|
||||
lum = _lum(blocks["light"][name])
|
||||
assert (lum + 0.05) / (ground + 0.05) >= 3, name
|
||||
assert (lum + 0.05) / 0.05 >= 3, name
|
||||
|
||||
@@ -31,11 +31,13 @@ def test_d03_parts():
|
||||
three = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_3D.ts")
|
||||
css = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_Style.css")
|
||||
draw = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_Draw.ts")
|
||||
draw_panel = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPanel.ts")
|
||||
tools = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts")
|
||||
layers = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_Layers.ts")
|
||||
assert 'createElement("progress")' not in ts and "createProgressBar()" in ts
|
||||
assert 'createElement("progress")' not in ts and "createProgressBar(" not in ts
|
||||
assert "createProgressCircle({ size: 40, corner: true })" in ts # 596-3 받기 = 지도 귀퉁이 서클
|
||||
# 562-3 — 날 단추 · 입력 · 고르기 없음 · 도구줄 · 제약 칸 · 연도 고름은 부품
|
||||
for src in (ts, draw, tools, layers):
|
||||
for src in (ts, draw, draw_panel, tools, layers):
|
||||
for raw in ('createElement("button")', 'createElement("input")', 'createElement("select")'):
|
||||
assert raw not in src, raw
|
||||
# 568-3 — 도구줄 = 공용 지도 도구줄 · 아홉([?] · 되돌리기 · 다시하기 · 3D · 돌리기 둘 · 측점 · 기울기 · 재기)
|
||||
@@ -77,7 +79,14 @@ def test_d03_parts():
|
||||
("place", "false"),
|
||||
], keys
|
||||
assert ".d03-plan__layers" in css and ".d03-plan__legend" in css
|
||||
assert "createSegment({" in draw and draw.count("createInputField(") == 2
|
||||
# 599-1 — 방식 칸 = 마디 고름 2 × 2 · 길이 · 기울기 두 열 · 노드 판(598-4)과 같은 함수
|
||||
assert "createSegment({" in draw_panel and draw_panel.count("createInputField(") == 2
|
||||
assert "columns: 2" in draw_panel and "export function createDrawKindFields(" in draw_panel
|
||||
assert ".d03-plan__draw-fields" in css and "createDrawKindFields(" in draw
|
||||
assert '"길이·기울기 고정"' in draw_panel and '"둘 다"' not in draw_panel
|
||||
assert "d03-plan__hint" not in draw # 칸 설명 글 걷음(요구 463-6)
|
||||
# 599-4 — 연도 구역 마커 = 그리기 칸 단추(끌어 놓기) · 지도 도구줄에서 뺌
|
||||
assert "YEAR_MARK_DRAG" in draw_panel and '"year-mark"' not in tools
|
||||
props = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_Props.ts") # 585-2 조각 연도 = 속성 판
|
||||
assert "createSelectField(" in props and "createFloatPanel(" in props and "createKv(" in props
|
||||
assert 'createElement("button")' not in props and 'createElement("select")' not in props
|
||||
|
||||
@@ -55,7 +55,6 @@ def test_단추_목록_아이콘_차례():
|
||||
"footprint",
|
||||
"measure",
|
||||
"station-snap",
|
||||
"year-mark",
|
||||
]
|
||||
names = _icon_names()
|
||||
assert all(icon in names for _, icon, _ in rows)
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
"""D03 다시 긋기 · 노드 기록(PLAN 599-2 · 599-3 · 요구 463-11 · 12) — TS `logic/D03_PlanAnalysis_Span.ts` · `_Plan.ts`.
|
||||
|
||||
두 꺾임점 사이를 새 점으로 바꿈 · 끝 두 점 그대로 · 뒤 갈래 차례 밈 · 속 점 갈래 = 막음 ·
|
||||
마크 = 바뀐 구간 안 길이 비율 · 뒤는 길이 차 · 조각 id 그대로 · 노드 기록 = 꼭짓점과 같은 길이로 따라감
|
||||
(더함 · 끼움 · 뺌 · 지움 · 뒤집기 · 잇기 · 다시 긋기) · 옮긴 노드 = 기록 남고 어긋남.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_599_3_plan_span.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
TS_FILE = ROOT / "D03_PlanAnalysis" / "logic" / "D03_PlanAnalysis_Span.ts"
|
||||
TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
||||
|
||||
_RUNNER = """
|
||||
import { writeFileSync } from "node:fs";
|
||||
import * as plan from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_Plan.js";
|
||||
import * as span from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_Span.js";
|
||||
const Y = 2026;
|
||||
let n = plan.newLine(plan.emptyNetwork(), [0, 0]);
|
||||
for (const p of [[100, 0], [200, 0], [300, 0]]) n = plan.appendVertex(n, 0, p);
|
||||
n = plan.normalizePieces(n, Y);
|
||||
n = plan.normalizePieces(plan.splitPiece(n, "main", 150), Y);
|
||||
n = plan.normalizePieces(plan.splitPiece(n, "main", 250), Y);
|
||||
n = plan.addBranch(n, 0, 2);
|
||||
n = plan.normalizePieces(plan.appendVertex(n, 1, [200, 80]), Y);
|
||||
const done = span.replaceSpan(n, 0, 1, 2, [[150, 50]]);
|
||||
const wide = span.replaceSpan(n, 0, 0, 3, [[150, 50]]);
|
||||
const bad = span.replaceSpan(n, 0, 2, 2, []);
|
||||
const straight = span.replaceSpan(n, 0, 0, 2, []);
|
||||
// 599-2 노드 기록
|
||||
const L = span.nodeRecord("length", 20, 9);
|
||||
const S = span.nodeRecord("slope", 20, 9);
|
||||
let r = plan.newLine(plan.emptyNetwork(), [0, 0]);
|
||||
r = plan.appendVertex(r, 0, [10, 0]);
|
||||
r = plan.appendVertex(r, 0, [30, 0], L);
|
||||
r = plan.appendVertex(r, 0, [30, 5], S);
|
||||
const recorded = r;
|
||||
const inserted = plan.insertVertex(r, 0, 0, [5, 0]);
|
||||
const dropped = plan.dropLastVertex(r, 0);
|
||||
const removed = plan.removeVertex(r, 0, 1);
|
||||
const reversed = plan.reverseLine(r, 0);
|
||||
let t = plan.appendVertex(plan.newLine(r, [30, 5]), 1, [40, 5], L);
|
||||
const joined = plan.attachHead(t, 1);
|
||||
const moved = plan.moveVertex(r, 0, 2, [33, 0]);
|
||||
const flat = () => 0;
|
||||
const off = [1, 2, 3].map((v) => span.nodeOff(moved.lines[0], v, flat));
|
||||
const respanned = span.replaceSpan(r, 0, 1, 3, [[20, 9]], [L]);
|
||||
const plain = span.replaceSpan(n, 0, 1, 2, [[150, 50]]);
|
||||
writeFileSync(
|
||||
process.argv[2],
|
||||
JSON.stringify({
|
||||
before: n, done, wide, bad, straight,
|
||||
recorded, inserted, dropped, removed, reversed, joined, moved, off, respanned, plain,
|
||||
}),
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
def _run_ts(tmp_path: Path) -> dict:
|
||||
out = tmp_path / "js"
|
||||
subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
[
|
||||
"node",
|
||||
str(TSC),
|
||||
str(TS_FILE),
|
||||
"--outDir",
|
||||
str(out),
|
||||
"--rootDir",
|
||||
str(ROOT),
|
||||
"--module",
|
||||
"esnext",
|
||||
"--target",
|
||||
"es2022",
|
||||
"--moduleResolution",
|
||||
"bundler",
|
||||
"--ignoreConfig",
|
||||
],
|
||||
cwd=str(ROOT),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
for emitted in out.rglob("*.js"):
|
||||
emitted.write_text(
|
||||
re.sub(r'(from\s+"\.\.?/[^"]+)"', r'\1.js"', emitted.read_text(encoding="utf-8")),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(out / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
|
||||
result = tmp_path / "out.json"
|
||||
subprocess.run( # noqa: S603
|
||||
["node", str(out / "runner.mjs"), str(result)],
|
||||
cwd=str(ROOT),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
return json.loads(result.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_replace_span(tmp_path: Path) -> None:
|
||||
r = _run_ts(tmp_path)
|
||||
done = r["done"]
|
||||
main = done["lines"][0]
|
||||
assert main["vertices"] == [[0, 0], [100, 0], [150, 50], [200, 0], [300, 0]]
|
||||
assert done["lines"][1]["branch"] == {"line": "main", "vertex": 3} # 뒤 갈래 한 칸 밈
|
||||
new_span = 2 * math.hypot(50, 50)
|
||||
pieces = [(p["id"], p["from_m"], p["to_m"]) for p in done["pieces"] if p["line"] == "main"]
|
||||
before = [p["id"] for p in r["before"]["pieces"] if p["line"] == "main"]
|
||||
assert [p[0] for p in pieces] == before # 조각 id 그대로
|
||||
assert pieces[1][1] == round(100 + new_span / 2, 3) # 구간 속 마크 = 길이 비율
|
||||
assert pieces[2][1] == round(250 - 200 + 100 + new_span, 3) # 뒤 마크 = 길이 차
|
||||
assert pieces[2][2] == round(300 - 200 + 100 + new_span, 3)
|
||||
|
||||
|
||||
def test_blocked_and_straight(tmp_path: Path) -> None:
|
||||
r = _run_ts(tmp_path)
|
||||
assert "갈래" in r["wide"] # 속 점(2)에 갈래
|
||||
assert "고르세요" in r["bad"]
|
||||
straight = r["straight"]["lines"][0]["vertices"]
|
||||
assert straight == [[0, 0], [200, 0], [300, 0]] # 속 점 없이 곧게
|
||||
assert r["straight"]["lines"][1]["branch"]["vertex"] == 1
|
||||
|
||||
|
||||
L = {"kind": "length", "length_m": 20, "slope_pct": None}
|
||||
S = {"kind": "slope", "length_m": None, "slope_pct": 9}
|
||||
F = {"kind": "free", "length_m": None, "slope_pct": None}
|
||||
|
||||
|
||||
def _nodes(network: dict, i: int = 0) -> list:
|
||||
line = network["lines"][i]
|
||||
assert len(line.get("nodes") or line["vertices"]) == len(line["vertices"]) # 서버 400 막음
|
||||
return line.get("nodes")
|
||||
|
||||
|
||||
def test_node_records_follow_vertices(tmp_path: Path) -> None:
|
||||
r = _run_ts(tmp_path)
|
||||
assert _nodes(r["recorded"]) == [None, None, L, S] # 첫 기록 = 앞 노드 기록 없음
|
||||
assert _nodes(r["inserted"]) == [None, None, None, L, S]
|
||||
assert _nodes(r["dropped"]) == [None, None, L]
|
||||
assert _nodes(r["removed"]) == [None, L, S]
|
||||
assert _nodes(r["reversed"]) == [None, S, L, None] # 토막 끝 노드 몫 = 한 칸 밀림
|
||||
assert len(r["joined"]["lines"]) == 1 and _nodes(r["joined"]) == [None, None, L, S, L]
|
||||
assert _nodes(r["moved"]) == [None, None, L, S] # 옮겨도 기록 남음
|
||||
# 2 = 길이 20 기록 · 지금 23 · 3 = 기울기 9 % 기록 · 평지 0 % → 둘 다 어긋남 · 1 = 기록 없음
|
||||
assert r["off"] == [False, True, True]
|
||||
assert _nodes(r["respanned"]) == [None, None, L, F] # 속 점 = 그때 기록 · 뒤 노드 = 자유
|
||||
assert "nodes" not in r["plain"]["lines"][0] # 기록 없는 선 = 그대로 없음
|
||||
@@ -0,0 +1,138 @@
|
||||
"""D03 거리재기 「노선 따라」 셈(PLAN 600-2 · 요구 463-23) — `logic/D03_PlanAnalysis_Measure.ts`.
|
||||
|
||||
같은 선 = 누가 차 · 갈래 ↔ 본선 = 갈래 고리를 따라 올라가 만나는 점까지 · 이어지지 않은 본선끼리 = null.
|
||||
TS 를 tsc 로 옮겨 node 로 돌림(서버 · 화면 없이).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_600_2_plan_measure.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
TS_FILE = ROOT / "D03_PlanAnalysis" / "logic" / "D03_PlanAnalysis_Measure.ts"
|
||||
TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
||||
|
||||
# main 200 m (0,0)→(100,0)→(100,100) · b1 = main 꼭짓점 1 에서 60 m · b2 = b1 끝에서 40 m · other = 이어지지 않은 본선
|
||||
NETWORK = {
|
||||
"pieces": [],
|
||||
"lines": [
|
||||
{"id": "main", "vertices": [[0, 0], [100, 0], [100, 100]], "branch": None},
|
||||
{"id": "b1", "vertices": [[100, 0], [160, 0]], "branch": {"line": "main", "vertex": 1}},
|
||||
{"id": "b2", "vertices": [[160, 0], [160, 40]], "branch": {"line": "b1", "vertex": 1}},
|
||||
{"id": "other", "vertices": [[0, 500], [50, 500]], "branch": None},
|
||||
],
|
||||
}
|
||||
|
||||
CASES = {
|
||||
"same": ["routeSpan", NETWORK, {"line": "main", "atM": 40}, {"line": "main", "atM": 150}],
|
||||
"same_back": ["routeSpan", NETWORK, {"line": "main", "atM": 150}, {"line": "main", "atM": 40}],
|
||||
"branch_main_after": [
|
||||
"routeSpan",
|
||||
NETWORK,
|
||||
{"line": "b1", "atM": 30},
|
||||
{"line": "main", "atM": 150},
|
||||
],
|
||||
"branch_main_before": [
|
||||
"routeSpan",
|
||||
NETWORK,
|
||||
{"line": "b1", "atM": 30},
|
||||
{"line": "main", "atM": 60},
|
||||
],
|
||||
"twig_root": ["routeSpan", NETWORK, {"line": "b2", "atM": 40}, {"line": "main", "atM": 0}],
|
||||
"twig_twig": ["routeSpan", NETWORK, {"line": "b2", "atM": 10}, {"line": "b1", "atM": 20}],
|
||||
"apart": ["routeSpan", NETWORK, {"line": "main", "atM": 10}, {"line": "other", "atM": 10}],
|
||||
"near_branch": ["nearestOnRoute", NETWORK, [130, 5], {"line": "main", "atM": 10}],
|
||||
"near_other_excluded": ["nearestOnRoute", NETWORK, [20, 490], {"line": "main", "atM": 10}],
|
||||
}
|
||||
|
||||
_RUNNER = """
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import * as measure from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_Measure.js";
|
||||
const cases = JSON.parse(readFileSync(process.argv[2], "utf8"));
|
||||
const out = {};
|
||||
for (const [key, [name, ...args]] of Object.entries(cases)) out[key] = measure[name](...args);
|
||||
writeFileSync(process.argv[3], JSON.stringify(out));
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def r(tmp_path_factory: pytest.TempPathFactory) -> dict:
|
||||
tmp = tmp_path_factory.mktemp("measure")
|
||||
out = tmp / "js"
|
||||
subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
[
|
||||
"node",
|
||||
str(TSC),
|
||||
str(TS_FILE),
|
||||
"--outDir",
|
||||
str(out),
|
||||
"--rootDir",
|
||||
str(ROOT),
|
||||
"--module",
|
||||
"esnext",
|
||||
"--target",
|
||||
"es2022",
|
||||
"--moduleResolution",
|
||||
"bundler",
|
||||
"--ignoreConfig",
|
||||
],
|
||||
cwd=str(ROOT),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
for emitted in out.rglob("*.js"):
|
||||
emitted.write_text(
|
||||
re.sub(r'(from\s+"\.\.?/[^"]+)"', r'\1.js"', emitted.read_text(encoding="utf-8")),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(out / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
|
||||
job, result = tmp / "job.json", tmp / "out.json"
|
||||
job.write_text(json.dumps(CASES), encoding="utf-8")
|
||||
subprocess.run( # noqa: S603
|
||||
["node", str(out / "runner.mjs"), str(job), str(result)],
|
||||
cwd=str(ROOT),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
return json.loads(result.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_같은_선은_누가_차(r):
|
||||
assert r["same"]["lengthM"] == pytest.approx(110)
|
||||
assert r["same"]["stations"] == [40, 150]
|
||||
assert r["same"]["path"][0] == [40, 0]
|
||||
assert r["same"]["path"][-1] == [100, 50]
|
||||
assert r["same_back"]["lengthM"] == pytest.approx(110)
|
||||
assert r["same_back"]["path"][0] == [100, 50]
|
||||
|
||||
|
||||
def test_갈래에서_본선으로(r):
|
||||
"""갈래 30 m 지점 → 갈래 첫 점(30) → 본선 100 m → 150 m(50) = 80 · 본선 60 m 쪽 = 30 + 40 = 70."""
|
||||
assert r["branch_main_after"]["lengthM"] == pytest.approx(80)
|
||||
assert r["branch_main_after"]["stations"] is None
|
||||
assert r["branch_main_after"]["path"][0] == [130, 0]
|
||||
assert r["branch_main_before"]["lengthM"] == pytest.approx(70)
|
||||
|
||||
|
||||
def test_갈래의_갈래는_고리를_따라_올라감(r):
|
||||
"""b2 끝(40) → b1 끝(160,0)까지 → b1 첫 점(60) → 본선 0 m(100) = 200 m."""
|
||||
assert r["twig_root"]["lengthM"] == pytest.approx(200)
|
||||
# b2 10 m(160,10) → b2 첫 점 10 → b1 20 m(120,0) 까지 40 = 50
|
||||
assert r["twig_twig"]["lengthM"] == pytest.approx(50)
|
||||
|
||||
|
||||
def test_이어지지_않은_본선은_길_없음(r):
|
||||
assert r["apart"] is None
|
||||
|
||||
|
||||
def test_가까운_자리는_이어진_선_안에서만(r):
|
||||
assert r["near_branch"]["line"] == "b1"
|
||||
assert r["near_branch"]["atM"] == pytest.approx(30)
|
||||
assert r["near_other_excluded"]["line"] == "main"
|
||||
Reference in New Issue
Block a user