feat(E): 단계 차례 보고서 맨 뒤 · DB 단계 행 여섯 · 체인 단계 상태 함수 (PLAN 565-1)
- 차례 = 지반 · 2D · 3D · 암반 · 광역 · 보고서 — STAB_STEP_ROUTES · 단계 막대 이름 · 아이콘 · 대시보드 요약 이름 같은 차례 - 단계 행 STABILITY 여섯(STAB_AREA 더함) · 022_stab_stages_six.sql = 옛 행 열쇠로 번호만 바꿈 · 없는 광역 행 NOT_STARTED · 다시 돌려도 같음 - mark_stab_step(project_id, step, state, message) — 체인 낱말(ground · 2d · 3d · rock · area · report × run · done · skip · error · cancel) → 단계 행 상태 · 열쇠로 찾음 · 실패 까닭만 남김 - E05 빈 상태 글 = E02 [해석 시작] · 보고서 안내에 암반 · 확률 - 시험 528 단계 여섯 · 상태 함수 · 280_2 가짜 차례 여섯 + 진짜 차례 끝 = E05 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,11 +37,11 @@ const FLOWS: readonly GroupFlow[] = [
|
|||||||
"DE_Step_StabGround",
|
"DE_Step_StabGround",
|
||||||
"DE_Step_StabSection",
|
"DE_Step_StabSection",
|
||||||
"DE_Step_Stab3D",
|
"DE_Step_Stab3D",
|
||||||
"DE_Step_StabReport",
|
|
||||||
"DE_Step_StabRock",
|
"DE_Step_StabRock",
|
||||||
"DE_Step_StabArea",
|
"DE_Step_StabArea",
|
||||||
|
"DE_Step_StabReport",
|
||||||
],
|
],
|
||||||
icons: ["🪨", "📐", "🧊", "📄", "⛰️", "🗺️"],
|
icons: ["🪨", "📐", "🧊", "⛰️", "🗺️", "📄"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ THUMB_POINTS = 100
|
|||||||
STAGE_NAMES: dict[str, tuple[str, ...]] = {
|
STAGE_NAMES: dict[str, tuple[str, ...]] = {
|
||||||
"ROAD": ("파일입력", "전처리", "노선설계", "횡단설계", "상세설계", "수량산출", "원가계산"),
|
"ROAD": ("파일입력", "전처리", "노선설계", "횡단설계", "상세설계", "수량산출", "원가계산"),
|
||||||
"PLAN": ("기준 지정", "노선 분석", "확정 · 내보내기"),
|
"PLAN": ("기준 지정", "노선 분석", "확정 · 내보내기"),
|
||||||
"STABILITY": ("지반 조건", "2D 해석", "3D 해석", "보고서", "암반 해석", "광역 위험"),
|
"STABILITY": ("지반 조건", "2D 해석", "3D 해석", "암반 해석", "광역 위험", "보고서"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function projectTable(projects: ProjectItem[], currentUser: DashboardUser
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 단계 막대 — 종류별 단계(ROAD = B03~B09 · PLAN = D02~D04 · STABILITY = E02~E05) · 누름 = 그 단계 화면 */
|
/** 단계 막대 — 종류별 단계(ROAD = B03~B09 · PLAN = D02~D04 · STABILITY = E02 · E03 · E04 · E06 · E07 · E05) · 누름 = 그 단계 화면 */
|
||||||
export function workflow(project: ProjectItem): HTMLElement {
|
export function workflow(project: ProjectItem): HTMLElement {
|
||||||
const group = groupFlowOf(project.project_kind);
|
const group = groupFlowOf(project.project_kind);
|
||||||
const routes = group?.routes ?? WORKFLOW_STEP_ROUTES;
|
const routes = group?.routes ?? WORKFLOW_STEP_ROUTES;
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ def gather(project_root: Path) -> dict[str, Any]:
|
|||||||
project_root = Path(project_root)
|
project_root = Path(project_root)
|
||||||
r2 = _result2d(project_root)
|
r2 = _result2d(project_root)
|
||||||
if not r2 or not r2.get("stations"):
|
if not r2 or not r2.get("stations"):
|
||||||
raise MissingInput("2D 해석 결과가 없습니다 — E03 에서 [해석] 뒤 만드세요.")
|
raise MissingInput("2D 해석 결과가 없습니다 — E02 에서 [해석 시작] 뒤 만드세요.")
|
||||||
root = source.source_dir(project_root)
|
root = source.source_dir(project_root)
|
||||||
db = source.db_snapshot(root)
|
db = source.db_snapshot(root)
|
||||||
ground = _ground(project_root)
|
ground = _ground(project_root)
|
||||||
|
|||||||
@@ -20,11 +20,37 @@ STAGE_KEYS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# 종류별 단계(요구 345) — ROAD = B03~B09 · PLAN = D02~D04 · STABILITY = E02~E06.
|
# 종류별 단계(요구 345 · 390) — ROAD = B03~B09 · PLAN = D02~D04 ·
|
||||||
|
# STABILITY = E02 · E03 · E04 · E06 · E07 · E05(보고서 맨 뒤 · 옛 행 = 022 SQL 로 다시 깖).
|
||||||
STAGE_KEYS_BY_KIND = {
|
STAGE_KEYS_BY_KIND = {
|
||||||
"ROAD": STAGE_KEYS,
|
"ROAD": STAGE_KEYS,
|
||||||
"PLAN": ["PLAN_BASE", "PLAN_ANALYSIS", "PLAN_CONFIRM"],
|
"PLAN": ["PLAN_BASE", "PLAN_ANALYSIS", "PLAN_CONFIRM"],
|
||||||
"STABILITY": ["STAB_GROUND", "STAB_SECTION", "STAB_3D", "STAB_REPORT", "STAB_ROCK"],
|
"STABILITY": [
|
||||||
|
"STAB_GROUND",
|
||||||
|
"STAB_SECTION",
|
||||||
|
"STAB_3D",
|
||||||
|
"STAB_ROCK",
|
||||||
|
"STAB_AREA",
|
||||||
|
"STAB_REPORT",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# E 체인 단계 열쇠(`E02_StabGround_Chain.STEPS` + 지반) → 단계 행 열쇠
|
||||||
|
STAB_STEP_STAGE_KEYS = {
|
||||||
|
"ground": "STAB_GROUND",
|
||||||
|
"2d": "STAB_SECTION",
|
||||||
|
"3d": "STAB_3D",
|
||||||
|
"rock": "STAB_ROCK",
|
||||||
|
"area": "STAB_AREA",
|
||||||
|
"report": "STAB_REPORT",
|
||||||
|
}
|
||||||
|
# 체인 단계 상태 → 단계 행 상태(건너뜀 = 저장본이 입력과 같음 · 끊음 = 옛 결과 낡았을 수 있음)
|
||||||
|
STAB_STEP_STATES = {
|
||||||
|
"run": "IN_PROGRESS",
|
||||||
|
"done": "COMPLETE",
|
||||||
|
"skip": "COMPLETE",
|
||||||
|
"error": "FAILED",
|
||||||
|
"cancel": "STALE",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -208,6 +234,66 @@ async def mark_stage(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def mark_stab_step(
|
||||||
|
project_id: str,
|
||||||
|
step: str,
|
||||||
|
state: str,
|
||||||
|
message: str | None = None,
|
||||||
|
cursor: aiomysql.DictCursor | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""E 안정성평가 단계 상태 한 곳(요구 390 · PLAN 565-1) — 대시보드 막대 색.
|
||||||
|
|
||||||
|
`step` = `STAB_STEP_STAGE_KEYS` 열쇠 · `state` = `STAB_STEP_STATES` 열쇠(체인 낱말 그대로) ·
|
||||||
|
`message` = 실패 까닭(행 칸 길이로 자름). 커서 없으면 제 트랜잭션 · DB 없으면 아무것도 안 함.
|
||||||
|
뒤 단계를 STALE 로 돌리지 않음(체인이 차례로 다 돎).
|
||||||
|
"""
|
||||||
|
stage_key = STAB_STEP_STAGE_KEYS[step]
|
||||||
|
row_state = STAB_STEP_STATES[state]
|
||||||
|
if cursor is None:
|
||||||
|
from config import config_db
|
||||||
|
|
||||||
|
if config_db.db_pool is None:
|
||||||
|
return
|
||||||
|
async with config_db.get_db_pool().acquire() as connection:
|
||||||
|
await connection.begin()
|
||||||
|
try:
|
||||||
|
async with connection.cursor() as own:
|
||||||
|
await mark_stab_step(project_id, step, state, message, own)
|
||||||
|
await connection.commit()
|
||||||
|
except Exception:
|
||||||
|
await connection.rollback()
|
||||||
|
raise
|
||||||
|
return
|
||||||
|
now = datetime.utcnow()
|
||||||
|
trimmed = (message or "").strip()[:_STAGE_MESSAGE_MAX_LENGTH] or None
|
||||||
|
await cursor.execute(
|
||||||
|
"""
|
||||||
|
UPDATE project_workflow_stages
|
||||||
|
SET state = %s,
|
||||||
|
progress_percent = CASE WHEN %s = 'COMPLETE' THEN 100
|
||||||
|
WHEN %s = 'IN_PROGRESS' THEN 0 ELSE progress_percent END,
|
||||||
|
message = CASE WHEN message LIKE 'DEV_UNLOCK:%%' THEN message ELSE %s END,
|
||||||
|
started_at = CASE WHEN %s = 'IN_PROGRESS' THEN %s ELSE started_at END,
|
||||||
|
completed_at = CASE WHEN %s = 'COMPLETE' THEN %s
|
||||||
|
WHEN %s = 'IN_PROGRESS' THEN NULL ELSE completed_at END
|
||||||
|
WHERE project_id = %s AND stage_key = %s
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
row_state,
|
||||||
|
row_state,
|
||||||
|
row_state,
|
||||||
|
trimmed if row_state == "FAILED" else None,
|
||||||
|
row_state,
|
||||||
|
now,
|
||||||
|
row_state,
|
||||||
|
now,
|
||||||
|
row_state,
|
||||||
|
project_id,
|
||||||
|
stage_key,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def update_stage_progress(
|
async def update_stage_progress(
|
||||||
cursor: aiomysql.DictCursor, project_id: str, stage_no: int, progress: int
|
cursor: aiomysql.DictCursor, project_id: str, stage_no: int, progress: int
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -141,14 +141,14 @@ export const PLAN_STEP_ROUTES = [
|
|||||||
ROUTES.D04_PLAN_CONFIRM,
|
ROUTES.D04_PLAN_CONFIRM,
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/** E 안정성평가 단계 주소(E02 → E06 · 광역 E07 · 생성 E01 은 단계 밖) */
|
/** E 안정성평가 단계 주소(E02 → E04 · 암반 E06 · 광역 E07 · 보고서 E05 맨 뒤 · 생성 E01 은 단계 밖) */
|
||||||
export const STAB_STEP_ROUTES = [
|
export const STAB_STEP_ROUTES = [
|
||||||
ROUTES.E02_STAB_GROUND,
|
ROUTES.E02_STAB_GROUND,
|
||||||
ROUTES.E03_STAB_SECTION,
|
ROUTES.E03_STAB_SECTION,
|
||||||
ROUTES.E04_STAB_3D,
|
ROUTES.E04_STAB_3D,
|
||||||
ROUTES.E05_STAB_REPORT,
|
|
||||||
ROUTES.E06_STAB_ROCK,
|
ROUTES.E06_STAB_ROCK,
|
||||||
ROUTES.E07_STAB_AREA,
|
ROUTES.E07_STAB_AREA,
|
||||||
|
ROUTES.E05_STAB_REPORT,
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type RouteKey = keyof typeof ROUTES;
|
export type RouteKey = keyof typeof ROUTES;
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
-- 022_stab_stages_six.sql
|
||||||
|
-- 안정성평가 단계 행 여섯 · 보고서 맨 뒤 (요구서 390 3장 5 · PLAN 565-1)
|
||||||
|
-- 차례 = STAB_GROUND 0 · STAB_SECTION 1 · STAB_3D 2 · STAB_ROCK 3 · STAB_AREA 4 · STAB_REPORT 5
|
||||||
|
-- 옛 행(다섯 · REPORT 3 · ROCK 4)은 열쇠로 번호만 바꿈(상태 그대로) · 없는 광역 행 = NOT_STARTED · 다시 돌려도 같음
|
||||||
|
|
||||||
|
USE aislo_db;
|
||||||
|
|
||||||
|
-- 1) 옛 번호 비켜 둠(uq_project_stage 겹침 피함)
|
||||||
|
UPDATE project_workflow_stages w
|
||||||
|
JOIN projects p ON p.id = w.project_id
|
||||||
|
SET w.stage_no = w.stage_no + 100
|
||||||
|
WHERE p.project_kind = 'STABILITY' AND w.stage_no < 100;
|
||||||
|
|
||||||
|
-- 2) 열쇠로 새 번호
|
||||||
|
UPDATE project_workflow_stages w
|
||||||
|
JOIN projects p ON p.id = w.project_id
|
||||||
|
SET w.stage_no = CASE w.stage_key
|
||||||
|
WHEN 'STAB_GROUND' THEN 0
|
||||||
|
WHEN 'STAB_SECTION' THEN 1
|
||||||
|
WHEN 'STAB_3D' THEN 2
|
||||||
|
WHEN 'STAB_ROCK' THEN 3
|
||||||
|
WHEN 'STAB_AREA' THEN 4
|
||||||
|
WHEN 'STAB_REPORT' THEN 5
|
||||||
|
END
|
||||||
|
WHERE p.project_kind = 'STABILITY' AND w.stage_no >= 100;
|
||||||
|
|
||||||
|
-- 3) 없는 광역 행
|
||||||
|
INSERT IGNORE INTO project_workflow_stages (project_id, stage_no, stage_key, state, progress_percent)
|
||||||
|
SELECT p.id, 4, 'STAB_AREA', 'NOT_STARTED', 0
|
||||||
|
FROM projects p
|
||||||
|
WHERE p.project_kind = 'STABILITY';
|
||||||
@@ -119,7 +119,15 @@ const STUBS = {
|
|||||||
ROUTES,
|
ROUTES,
|
||||||
API_BASE_URL: "",
|
API_BASE_URL: "",
|
||||||
PLAN_STEP_ROUTES: ["d02-plan-base", "d03-plan-analysis", "d04-plan-confirm"],
|
PLAN_STEP_ROUTES: ["d02-plan-base", "d03-plan-analysis", "d04-plan-confirm"],
|
||||||
STAB_STEP_ROUTES: ["e02-stab-ground", "e03-stab-section", "e04-stab-3d", "e05-stab-report"],
|
// 565-1 — 보고서(E05) 맨 뒤
|
||||||
|
STAB_STEP_ROUTES: [
|
||||||
|
"e02-stab-ground",
|
||||||
|
"e03-stab-section",
|
||||||
|
"e04-stab-3d",
|
||||||
|
"e06-stab-rock",
|
||||||
|
"e07-stab-area",
|
||||||
|
"e05-stab-report",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
"./ui_template_locale": { t: (key) => key },
|
"./ui_template_locale": { t: (key) => key },
|
||||||
"./ui_template_modal": { openModal: () => undefined },
|
"./ui_template_modal": { openModal: () => undefined },
|
||||||
@@ -202,6 +210,7 @@ const ALL = [
|
|||||||
"d04-plan-confirm",
|
"d04-plan-confirm",
|
||||||
"e01-stab-register",
|
"e01-stab-register",
|
||||||
"e02-stab-ground",
|
"e02-stab-ground",
|
||||||
|
"e07-stab-area",
|
||||||
"e05-stab-report",
|
"e05-stab-report",
|
||||||
];
|
];
|
||||||
const rows = {};
|
const rows = {};
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ def test_529_1_D_E_묶음_안에서만_앞뒤(results):
|
|||||||
assert rows["d04-plan-confirm"]["plain"]["disabled"][:3] == [False, False, True]
|
assert rows["d04-plan-confirm"]["plain"]["disabled"][:3] == [False, False, True]
|
||||||
assert rows["e02-stab-ground"]["plain"]["disabled"][:3] == [False, True, False]
|
assert rows["e02-stab-ground"]["plain"]["disabled"][:3] == [False, True, False]
|
||||||
assert rows["e05-stab-report"]["plain"]["disabled"][:3] == [False, False, True]
|
assert rows["e05-stab-report"]["plain"]["disabled"][:3] == [False, False, True]
|
||||||
|
# 565-1 — 광역(E07) 뒤에 보고서 · 진짜 차례도 E05 가 끝
|
||||||
|
assert rows["e07-stab-area"]["plain"]["disabled"][:3] == [False, False, False]
|
||||||
|
config = (ROOT / "config" / "config_frontend.ts").read_text(encoding="utf-8")
|
||||||
|
stab = config.split("export const STAB_STEP_ROUTES = [")[1].split("]")[0]
|
||||||
|
assert [s.strip() for s in stab.split(",") if s.strip()][-2:] == [
|
||||||
|
"ROUTES.E07_STAB_AREA",
|
||||||
|
"ROUTES.E05_STAB_REPORT",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("shape", ["helpRow", "plain"])
|
@pytest.mark.parametrize("shape", ["helpRow", "plain"])
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ from A07_Register.A07_Register_Schema import RegisterRequest
|
|||||||
from common_util import common_util_auth as auth
|
from common_util import common_util_auth as auth
|
||||||
from common_util import common_util_auth_repository as auth_repo
|
from common_util import common_util_auth_repository as auth_repo
|
||||||
from common_util.common_util_project_kind import KIND_STAGE_DIRS, init_kind_storage
|
from common_util.common_util_project_kind import KIND_STAGE_DIRS, init_kind_storage
|
||||||
from common_util.common_util_workflow_state import initialize_project_stages
|
from common_util.common_util_workflow_state import (
|
||||||
|
STAB_STEP_STAGE_KEYS,
|
||||||
|
STAGE_KEYS_BY_KIND,
|
||||||
|
initialize_project_stages,
|
||||||
|
mark_stab_step,
|
||||||
|
)
|
||||||
from config.config_system import PROJECT_STORAGE_STAGE_DIRS
|
from config.config_system import PROJECT_STORAGE_STAGE_DIRS
|
||||||
from D01_PlanRegister import D01_PlanRegister_Router as d01
|
from D01_PlanRegister import D01_PlanRegister_Router as d01
|
||||||
from E01_StabRegister import E01_StabRegister_Router as e01
|
from E01_StabRegister import E01_StabRegister_Router as e01
|
||||||
@@ -261,13 +266,38 @@ def test_register_ignores_sent_company():
|
|||||||
assert "company_name" not in data
|
assert "company_name" not in data
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("kind,count", [("ROAD", 7), ("PLAN", 3), ("STABILITY", 5)])
|
@pytest.mark.parametrize("kind,count", [("ROAD", 7), ("PLAN", 3), ("STABILITY", 6)])
|
||||||
def test_stage_seed_by_kind(kind, count):
|
def test_stage_seed_by_kind(kind, count):
|
||||||
log = []
|
log = []
|
||||||
asyncio.run(initialize_project_stages(FakeCursor({}, log), "p1", kind))
|
asyncio.run(initialize_project_stages(FakeCursor({}, log), "p1", kind))
|
||||||
assert len(log) == count
|
assert len(log) == count
|
||||||
if kind == "PLAN":
|
if kind == "PLAN":
|
||||||
assert [a[2] for _, a in log] == ["PLAN_BASE", "PLAN_ANALYSIS", "PLAN_CONFIRM"]
|
assert [a[2] for _, a in log] == ["PLAN_BASE", "PLAN_ANALYSIS", "PLAN_CONFIRM"]
|
||||||
|
if kind == "STABILITY": # 565-1 — 보고서 맨 뒤
|
||||||
|
assert [a[2] for _, a in log][3:] == ["STAB_ROCK", "STAB_AREA", "STAB_REPORT"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_565_1_stab_step_marks_by_key():
|
||||||
|
"""체인 낱말 → 행 상태 · 열쇠로 찾음 · 실패 까닭만 남김 · 모르는 낱말 거절."""
|
||||||
|
log = []
|
||||||
|
for step, state, message in (
|
||||||
|
("3d", "run", None),
|
||||||
|
("3d", "skip", None),
|
||||||
|
("area", "error", "x" * 300),
|
||||||
|
("report", "cancel", None),
|
||||||
|
):
|
||||||
|
asyncio.run(mark_stab_step("p1", step, state, message, FakeCursor({}, log)))
|
||||||
|
rows = [(args[0], args[3], args[-1]) for _, args in log]
|
||||||
|
assert rows == [
|
||||||
|
("IN_PROGRESS", None, "STAB_3D"),
|
||||||
|
("COMPLETE", None, "STAB_3D"),
|
||||||
|
("FAILED", "x" * 200, "STAB_AREA"),
|
||||||
|
("STALE", None, "STAB_REPORT"),
|
||||||
|
]
|
||||||
|
assert all("stage_key = %s" in sql for sql, _ in log)
|
||||||
|
assert set(STAB_STEP_STAGE_KEYS.values()) == set(STAGE_KEYS_BY_KIND["STABILITY"])
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
asyncio.run(mark_stab_step("p1", "2d", "weird", None, FakeCursor({}, log)))
|
||||||
|
|
||||||
|
|
||||||
def test_storage_dirs_allowed():
|
def test_storage_dirs_allowed():
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import type { LocaleEntry } from "./ui_template_locale_common";
|
|||||||
export const ui_locales_e05 = {
|
export const ui_locales_e05 = {
|
||||||
/* --- E05 보고서 --- */
|
/* --- E05 보고서 --- */
|
||||||
E05_Note: [
|
E05_Note: [
|
||||||
"표지 · 개요는 사용자 몫 — [보고서 갱신] 때 그대로 둡니다. 목차 · 입력조건 · 측점표 · 대표단면 · 3D · 광역 · 보강 · 부록은 해석 결과로 다시 씁니다.",
|
"표지 · 개요는 사용자 몫 — [보고서 갱신] 때 그대로 둡니다. 목차 · 입력조건 · 측점표 · 대표단면 · 3D · 광역 · 보강 · 암반 · 확률 · 부록은 해석 결과로 다시 씁니다.",
|
||||||
"Cover and summary are yours — kept on [Refresh report]. Contents, conditions, stations, sections, 3D, area, reinforcement and appendix sheets are rewritten from results.",
|
"Cover and summary are yours — kept on [Refresh report]. Contents, conditions, stations, sections, 3D, area, reinforcement, rock, probability and appendix sheets are rewritten from results.",
|
||||||
],
|
],
|
||||||
E05_Btn_Refresh: ["보고서 갱신", "Refresh report"],
|
E05_Btn_Refresh: ["보고서 갱신", "Refresh report"],
|
||||||
E05_Btn_Xlsx: ["엑셀", "Excel"],
|
E05_Btn_Xlsx: ["엑셀", "Excel"],
|
||||||
|
|||||||
Reference in New Issue
Block a user