From 7c3a14fc9bd6d72f7a275d5a3be38c1afa4f2f32 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 21 Oct 2026 15:43:10 +0900 Subject: [PATCH 1/5] =?UTF-8?q?chore(=EC=9A=94=EA=B5=AC):=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=20312=20=E2=80=94=20=EA=B3=84=EC=82=B0=20=EC=9E=90?= =?UTF-8?q?=EB=A6=AC=20=EC=B0=BD=2045=EC=A4=84=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/raw/요구_1021/사용자요구_1021.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/raw/요구_1021/사용자요구_1021.md b/docs/raw/요구_1021/사용자요구_1021.md index 600f75e0b..9858b60d4 100644 --- a/docs/raw/요구_1021/사용자요구_1021.md +++ b/docs/raw/요구_1021/사용자요구_1021.md @@ -21,3 +21,8 @@ 4. b05에서 mc는 사용자가 수정해야할 부분임. 이때 mc도 붙이고 글자크기 키우고 볼트체와 색상을 변경. b07~b09에도 남길것. 5. 검증 스크립트는 프로그램 개발에 도움을 주기 위한 스크립트는 너의 재량것 사용. 6. 서브3 백업사본은 삭제 + +## 요구 312 (10-21 · 사용자 · 계산 자리 창) + +1. 계산위치 제어 페이지사 제어안됨? 추가로로직은 45개 아님? +2. (물음 답) 45줄 전체 표시 From 254b5267fff3282df449a0e23854d4a892762497 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 21 Oct 2026 15:49:05 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix(507-1=20=EB=85=B8=EC=B6=9C=ED=98=95=20?= =?UTF-8?q?=ED=9A=A1=EB=8B=A8=EC=88=98=EB=A1=9C=20=C2=B7=20=EA=B0=9C?= =?UTF-8?q?=EA=B1=B0=20=3D=20=ED=9A=A1=EB=8B=A8=EB=A9=B4=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=EB=A7=8C):=20=EB=B6=80=ED=92=88=20=EC=83=81=EC=9E=90?= =?UTF-8?q?=20=ED=98=95=EC=83=81=20=E3=80=8C=ED=91=9C=EC=8B=9C=EB=A7=8C?= =?UTF-8?q?=E3=80=8D(mark=20=C2=B7=20=EC=9D=BD=EB=8A=94=20=EC=B9=B8=20?= =?UTF-8?q?=EC=97=86=EC=9D=8C=20=C2=B7=20=EA=B7=B8=EB=A6=BC=20=EC=97=86?= =?UTF-8?q?=EC=9D=8C)=20=C2=B7=20=EB=91=90=20=EC=A2=85=EB=A5=98=20?= =?UTF-8?q?=EC=B2=98=EC=9D=8C=EA=B0=92=20=3D=20=EC=B8=A1=EC=A0=90=20?= =?UTF-8?q?=EC=84=B8=EC=9B=80(anchor)=20+=20=ED=91=9C=EC=8B=9C=EB=A7=8C=20?= =?UTF-8?q?=C2=B7=20B05=20=E3=80=8C=ED=9A=A1=EB=8B=A8=E3=80=8D=20=ED=8C=90?= =?UTF-8?q?=20=C2=B7=20=EC=9E=84=EC=8B=9C=20=ED=9A=A1=EB=8B=A8=20=EC=B0=BD?= =?UTF-8?q?=20=3D=20=EB=A8=B8=EB=A6=AC=20=EA=B1=B7=EA=B8=B0=20=EC=A0=84=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EC=9D=B4=EB=A6=84=20=EC=B9=A9=20?= =?UTF-8?q?=EB=82=A8=EA=B9=80=20=E2=80=94=20=EC=9A=94=EA=B5=AC=20311-2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자 마스터(구조물정의.json 반영) 안 고침 · M02 표 형상 칸에서 「표시만」 고름 - 시험 약속 바뀜: 322-1 처음값 · 272-1 · 322-3 측점 120 더함 · 322-2 218 규칙 보기 = 반사경 · 505-3 칩에 이름 - 새 시험 test_507_1_exposed_mark(.py · _screen) Co-Authored-By: Claude Opus 5.5 --- .../B05_Profile_UI_RouteEdit_Cross_Card.ts | 6 +- common_util/common_util_structure_parts.json | 14 +- resources/tester/helper_322_2_check.cjs | 6 +- resources/tester/test_272_1_station_list.py | 4 +- resources/tester/test_322_1_catalog_mirror.py | 4 +- resources/tester/test_322_2_check_mirror.py | 10 +- resources/tester/test_322_3_plan_mirror.py | 4 +- .../tester/test_505_3_cross_card_screen.py | 4 +- .../tester/test_505_3_cross_card_text.py | 2 +- resources/tester/test_507_1_exposed_mark.py | 74 +++++++ .../tester/test_507_1_exposed_mark_screen.py | 189 ++++++++++++++++++ 11 files changed, 295 insertions(+), 22 deletions(-) create mode 100644 resources/tester/test_507_1_exposed_mark.py create mode 100644 resources/tester/test_507_1_exposed_mark_screen.py diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Cross_Card.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Cross_Card.ts index f99e4050e..4e28bcb5d 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Cross_Card.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Cross_Card.ts @@ -156,8 +156,10 @@ export function routeCrossCard( box, ); // 그래프만(393-3) — 카드 머리 · 줌 단추 · 발(암 경계 · 요약) 걷음 · 벽 · 선 · 면적 고름 막음 - // 경고 칩(빈칸 · 기본값 · 사면 열림)만 그래프 오른쪽 위에 겹쳐 남김(505-3) - const chips = card.querySelectorAll(":scope > header .b06-cross-card__warning"); + // 경고 칩(빈칸 · 기본값 · 사면 열림) · 구조물 이름(표시만 종류 · 507-1)만 그래프 오른쪽 위에 겹쳐 남김(505-3) + const chips = card.querySelectorAll( + ":scope > header .b06-cross-card__warning, :scope > header .b06-cross-card__structure", + ); if (chips.length) { const box = document.createElement("div"); box.className = "b05-cross-chips"; diff --git a/common_util/common_util_structure_parts.json b/common_util/common_util_structure_parts.json index aba3e422f..1359dcc8e 100644 --- a/common_util/common_util_structure_parts.json +++ b/common_util/common_util_structure_parts.json @@ -150,6 +150,12 @@ "읽는 칸": ["pave_spec", "height_m", "widening_off"], "자리꼴": ["interval"], "정본": ["structures"] + }, + "mark": { + "이름": "표시만(형상 없음 · 측점 · 이름만)", + "읽는 칸": [], + "자리꼴": ["point", "interval", "site"], + "정본": ["pipe_points", "structures"] } }, "3D": { @@ -223,16 +229,16 @@ }, "cross_drain_exposed": { "목록": "both", - "횡단도": "none", + "횡단도": "anchor", "카드이름": "name", - "형상": "none", + "형상": "mark", "3D": "none" }, "open_ditch": { "목록": "both", - "횡단도": "none", + "횡단도": "anchor", "카드이름": "name", - "형상": "none", + "형상": "mark", "3D": "none" }, "cross_underdrain": { diff --git a/resources/tester/helper_322_2_check.cjs b/resources/tester/helper_322_2_check.cjs index 5d2b7c70f..d0a4c02eb 100644 --- a/resources/tester/helper_322_2_check.cjs +++ b/resources/tester/helper_322_2_check.cjs @@ -53,8 +53,8 @@ const enabled = (id) => def.types.find((t) => t.type_id === id).enabled; const own = (id) => clone(reflect.reflectOf(def)[id] ?? null); reflect.setReflect(def, "wildlife_path", "목록", "both"); reflect.setReflect(def, "retaining_wall", "목록", "b06"); -reflect.setReflect(def, "open_ditch", "횡단도", "none"); -reflect.setReflect(def, "cross_drain_exposed", "횡단도", "anchor"); +reflect.setReflect(def, "open_ditch", "횡단도", "anchor"); +reflect.setReflect(def, "cross_drain_exposed", "횡단도", "none"); const setBefore = reflect.isSet(def, "cross_drain_exposed", "횡단도"); reflect.setShapeOpt(def, "soil_guard", "weep", true); const weepOn = own("soil_guard"); @@ -95,8 +95,8 @@ const steps = []; reflect.setReflect(work, "masonry_wet", "목록", "both"); steps.push(reflect.differs(work, base)); reflect.setReflect(work, "masonry_wet", "목록", "hidden"); -reflect.setReflect(work, "open_ditch", "횡단도", "anchor"); reflect.setReflect(work, "open_ditch", "횡단도", "none"); +reflect.setReflect(work, "open_ditch", "횡단도", "anchor"); reflect.setShapeOpt(work, "soil_guard", "form", "콘크리트"); reflect.setShapeOpt(work, "soil_guard", "form", "통나무·목재틀"); reflect.setShown(work, "pipe", ["wing_wall_type"], false); diff --git a/resources/tester/test_272_1_station_list.py b/resources/tester/test_272_1_station_list.py index 845e8b0df..686445bb1 100644 --- a/resources/tester/test_272_1_station_list.py +++ b/resources/tester/test_272_1_station_list.py @@ -41,7 +41,7 @@ PIPES = [ }, ] STRUCTURES = [ - # 노출형 = 구조물 표 횡단도 none(322-1 ★1) — 측점 안 심음 + # 노출형 = 구조물 표 횡단도 anchor · 형상 표시만(507-1 · 요구 311-2) — 측점 심음 {"type_id": "cross_drain_exposed", "placement": "point", "chainage_m": 120.0}, { "type_id": "retaining_wall", @@ -67,7 +67,7 @@ STRUCTURES = [ }, {"type_id": "evac_road", "placement": "point", "chainage_m": 720.0}, ] -EXPECTED = [95, 160, 230, 300, 395, 400, 410, 500, 510, 530, 545, 560] +EXPECTED = [95, 120, 160, 230, 300, 395, 400, 410, 500, 510, 530, 545, 560] # 368-2 추가 측점 — 47 만 듦(95 관 · 300 세월교 자리는 그쪽이 이김) PLUS = [47.0, 95.0, 300.0] diff --git a/resources/tester/test_322_1_catalog_mirror.py b/resources/tester/test_322_1_catalog_mirror.py index 906428a7b..acccaf659 100644 --- a/resources/tester/test_322_1_catalog_mirror.py +++ b/resources/tester/test_322_1_catalog_mirror.py @@ -38,8 +38,8 @@ TABLE = { "box_culvert": ("both", "anchor", "name", "box", "box"), "ford_pavement": ("both", "anchor", "name", "ford_pavement", "chute"), "ford_bridge": ("both", "anchor", "name", "ford", "ford"), - "cross_drain_exposed": ("both", "none", "name", "none", "none"), - "open_ditch": ("both", "none", "name", "none", "none"), + "cross_drain_exposed": ("both", "anchor", "name", "mark", "none"), # 표시만(요구 311-2 · 507-1) + "open_ditch": ("both", "anchor", "name", "mark", "none"), # 표시만(요구 311-2 · 507-1) "cross_underdrain": ("both", "anchor", "name", "underdrain", "surface"), **{ t: ("hidden", "none", "name", "none", "none") diff --git a/resources/tester/test_322_2_check_mirror.py b/resources/tester/test_322_2_check_mirror.py index 518b48d31..45e1d7f02 100644 --- a/resources/tester/test_322_2_check_mirror.py +++ b/resources/tester/test_322_2_check_mirror.py @@ -63,7 +63,7 @@ def _cases() -> dict[str, tuple[dict, dict | None]]: bad["부품"] = {"형상.zz": False, "횡단도.span3": "no", "끔": False} cases["shape_bad"] = (bad, real) rule = copy.deepcopy(real) - rule["반영"] = {"open_ditch": {"횡단도": "anchor"}} + rule["반영"] = {"mirror": {"횡단도": "anchor"}} # 형상 없음(개거 = 표시만 · 507-1) cases["rule218"] = (rule, real) mismatch = copy.deepcopy(real) mismatch["반영"] = {"retaining_wall": {"형상": "pipe_set"}, "berm": {"횡단도": "anchor"}} @@ -126,7 +126,7 @@ def test_check_rules(ts: dict) -> None: "부품 「횡단도.span3」 — 참거짓", "부품 「끔」 — 없는 부품", ] - assert got["rule218"] == ["「개거(겉도랑)」 형상이 「없음」이면 횡단도도 「안 만듦」"] + assert got["rule218"] == ["「반사경」 형상이 「없음」이면 횡단도도 「안 만듦」"] assert got["mismatch"] == ["「옹벽」 형상 「배관 세트」 — 구간 · 구조물에 안 맞음"] assert got["hide_read"] == [ "「배관」 형상 「배관 세트」 이 쓰는 칸 「흙막이 기초」 — 숨길 수 없음" @@ -158,8 +158,8 @@ def test_reflect_edits(ts: dict) -> None: r = ts["reflect"] assert r["wildlife"] == [None, True] # b06 → both = enabled 참이 곧 처음 값 짝(반영 안 남음) assert r["wall"] == [None, False] # both → b06 = enabled 거짓 - assert r["openDitch"] is None # 처음 값(none)과 같음 — 반영 안 남음 - assert r["exposed"] == [{"횡단도": "anchor"}, True] + assert r["openDitch"] is None # 처음 값(anchor · 표시만 507-1)과 같음 — 반영 안 남음 + assert r["exposed"] == [{"횡단도": "none"}, True] assert r["weepOn"] == {"형상칸": {"weep": True}} and r["weepOff"] is None assert r["shown"] == {"pipe_kind": False, "inlet_type": False} # 정의 옵션만 assert r["readPipe"] == ["inlet_basin_height_m", "revet_foundation"] # 기본값 없는 읽는 칸 @@ -233,7 +233,7 @@ def test_server_save_reflect_parts_color_enabled(copy_def: Path) -> None: @pytest.mark.parametrize( "change", [ - lambda d: d.update(반영={"open_ditch": {"횡단도": "anchor"}}), # 218 + lambda d: d.update(반영={"mirror": {"횡단도": "anchor"}}), # 218 lambda d: d["표시"].setdefault("pipe", {}).update(revet_foundation=False), # 못 숨김 칸 lambda d: next(o for o in _type(d, "pipe")["options"] if o["key"] == "pipe_kind")[ "choices" diff --git a/resources/tester/test_322_3_plan_mirror.py b/resources/tester/test_322_3_plan_mirror.py index 6854ae3ea..09cd2abbb 100644 --- a/resources/tester/test_322_3_plan_mirror.py +++ b/resources/tester/test_322_3_plan_mirror.py @@ -147,6 +147,7 @@ def test_plan_rules(ts: dict) -> None: mixed = ts["mixed"] assert [round(s["chainage_m"], 3) for s in mixed] == [ 95.0, + 120.0, # 노출형 횡단수로 = 표시만 측점(507-1) 160.0, 230.0, 395.0, @@ -164,7 +165,8 @@ def test_plan_rules(ts: dict) -> None: assert first["blank"] == ["pipe_diameter_mm", "culvert_height_m"] joint = next(s for s in mixed if s["chainage_m"] == 530.0) # 옹벽 끝 = 소단 시작 assert joint["owners"] == ["s2", "s3"] and joint["title"] == "옹벽" and joint["role"] == "end" - assert all(s["title"] != "노출형 횡단수로" for s in mixed) # ★1 횡단도 none + exposed = next(s for s in mixed if s["chainage_m"] == 120.0) # 표시만 = 이름 · 빈칸 없음(507-1) + assert exposed["title"] == "노출형 횡단수로" and exposed["blank"] == [] kinds = ts["pipe_kinds"] assert [s["title"] for s in kinds] == ["흄관 D800", "파형강관 D1000", "세월교"] assert kinds[1]["blank"] == ["pipe_diameter_mm"] and kinds[0]["blank"] == [] diff --git a/resources/tester/test_505_3_cross_card_screen.py b/resources/tester/test_505_3_cross_card_screen.py index 61dd405b5..872ef6687 100644 --- a/resources/tester/test_505_3_cross_card_screen.py +++ b/resources/tester/test_505_3_cross_card_screen.py @@ -175,7 +175,7 @@ def test_0_곡선_판_도구줄_횡단판_비킴(page): def test_1_빈칸_칩_카드(page): modal = _open_modal(page) assert modal["station"] == "19+8" and modal["card"] - assert modal["chips"] == ["⚠ 배관 관경 빈칸"] + assert modal["chips"] == ["⚠ 배관 관경 빈칸", "배수관"] # 이름 칩(507-1) assert modal["pipes"] == 0 and modal["width"] == 380 assert all("ui-btn ui-btn--ghost ui-btn--sm" in name for name in modal["buttons"]) page.locator(".b05-crossmodal").get_by_text("닫기").click() @@ -198,7 +198,7 @@ def test_2_횡단_판_칩_관경_넣으면_형상_확대_밝음_어두움(page): ).first.select_option("800") # 세션 초안만 — [저장] 안 누름 page.wait_for_timeout(5000) modal = _open_modal(page) - assert modal["chips"] == [] and modal["pipes"] > 0 + assert modal["chips"] == ["배수관"] and modal["pipes"] > 0 shot = page.locator(".b05-crossmodal") shot.screenshot(path=str(SHOTS / "modal_light.png")) page.evaluate("document.documentElement.setAttribute('data-theme', 'dark')") diff --git a/resources/tester/test_505_3_cross_card_text.py b/resources/tester/test_505_3_cross_card_text.py index 05ecc02e1..25e350e53 100644 --- a/resources/tester/test_505_3_cross_card_text.py +++ b/resources/tester/test_505_3_cross_card_text.py @@ -47,7 +47,7 @@ def test_판_재료_한_벌() -> None: def test_빈칸_칩은_머리_걷기_전에_꺼냄() -> None: card = _read("B05_Profile", "B05_Profile_UI_RouteEdit_Cross_Card.ts") - chips = card.index('":scope > header .b06-cross-card__warning"') + chips = card.index('":scope > header .b06-cross-card__warning') # 이름 칩도 같이(507-1) remove = card.index(':scope > header, :scope > footer, .b06-cross-card__zoom"') assert chips < remove assert 'box.className = "b05-cross-chips";' in card diff --git a/resources/tester/test_507_1_exposed_mark.py b/resources/tester/test_507_1_exposed_mark.py new file mode 100644 index 000000000..797c0a7b1 --- /dev/null +++ b/resources/tester/test_507_1_exposed_mark.py @@ -0,0 +1,74 @@ +"""PLAN 507-1 노출형 횡단수로 · 개거 = 횡단면 표시만(요구 311-2). + +- 부품 상자 형상 `mark`(표시만) — 읽는 칸 · 필수 칸 없음 → 빈칸 경고 없음 · 그리는 함수 없음. +- 처음값 = 횡단도 anchor(측점 세움) · 카드이름 name · 형상 mark · 3D none. +- 측점 계획 = 그 자리에 측점 하나 · 이름 = 종류 이름. +- 실제 정의(사용자 마스터 · 반영 없음) = M02 검사 오류 없음(218 규칙 = 형상 none 만 막음). +- B05 「횡단」 판 · 임시 횡단 창 = 카드 머리를 걷기 전에 구조물 이름 칩을 꺼내 남김. +""" + +from __future__ import annotations + +from pathlib import Path + +from B05_Profile.B05_Profile_Structures_Schema import StructureInstance, load_definition +from common_util.common_util_structure_catalog import ( + catalog, + load_parts, + resolve_catalog, + shape_needs, +) +from common_util.common_util_structure_station import plan_stations +from M02_MasterTemplete.StructOptions.M02_SO_Check import catalog_errors + +ROOT = Path(__file__).resolve().parents[2] +KINDS = {"cross_drain_exposed": "노출형 횡단수로", "open_ditch": "개거(겉도랑)"} + + +def test_mark_part_has_no_fields() -> None: + mark = load_parts()["parts"]["형상"]["mark"] + assert mark["읽는 칸"] == [] and "필수 칸" not in mark + assert "point" in mark["자리꼴"] and "structures" in mark["정본"] + + +def test_start_values_are_mark_only() -> None: + rows = resolve_catalog(load_definition()) + for type_id in KINDS: + row = rows[type_id] + got = (row["station"], row["title"], row["shape"], row["model3d"]) + assert got == ("anchor", "name", "mark", "none"), type_id + assert shape_needs(type_id) == [] + + +def test_station_planted_with_name() -> None: + structures = [ + StructureInstance.model_validate( + { + "structure_id": f"s{i}", + "type_id": type_id, + "placement": "point", + "chainage_m": at, + "options": {}, + } + ) + for i, (type_id, at) in enumerate((("cross_drain_exposed", 120.0), ("open_ditch", 240.0))) + ] + plan = plan_stations([], structures) + assert [(round(s["chainage_m"], 3), s["title"], s["blank"]) for s in plan] == [ + (120.0, KINDS["cross_drain_exposed"], []), + (240.0, KINDS["open_ditch"], []), + ] + + +def test_real_definition_passes_m02_check() -> None: + assert load_definition().get("반영") == {} # 사용자 마스터 값은 안 고침 + assert catalog_errors(load_definition(), None) == [] + assert all(catalog()[t]["shape"] == "mark" for t in KINDS) + + +def test_b05_card_keeps_structure_name_chip() -> None: + card = (ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit_Cross_Card.ts").read_text( + encoding="utf-8" + ) + keep = card.index(":scope > header .b06-cross-card__structure") + assert keep < card.index(':scope > header, :scope > footer, .b06-cross-card__zoom"') diff --git a/resources/tester/test_507_1_exposed_mark_screen.py b/resources/tester/test_507_1_exposed_mark_screen.py new file mode 100644 index 000000000..5a22d3a3a --- /dev/null +++ b/resources/tester/test_507_1_exposed_mark_screen.py @@ -0,0 +1,189 @@ +"""PLAN 507-1 화면 — 자기 포트(5179) Playwright 머리 없는 브라우저 · 서버가 떠 있을 때만 · 검증_sub4. + +- B05 「구조물 배치」 노출형 횡단수로 7+7(147 m) 추가(세션만 · [저장] 안 함) → 측점 목록에 섬 +- 「횡단」 판 · 임시 횡단 창 = B06 카드 + 이름 칩 「노출형 횡단수로」 · 구조물 형상 0 +- B06 횡단 카드 머리 = 이름 · 형상 0 → 지움(세션) · 측점 목록에서 빠짐 +- 계정 화면 취향만 시험 앞 값으로 되돌림 · DB · storage 안 씀 + + ./venv/Scripts/python.exe -m pytest resources/tester/test_507_1_exposed_mark_screen.py -q +""" + +from __future__ import annotations + +import json +import urllib.request +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +BASE = "http://localhost:5179" +PROJECT = "c725d747-50ae-46b0-87ce-4d4e56a53cf3" # 검증_sub4(OWNERS.md) +SHOTS = ROOT / "tmp" / "shots_507_1" +AT_M = 147 # 7+7 — 격자 · 관(19+8) 측점과 안 겹침 +NAME = "노출형 횡단수로" +PREFS = "/api/dashboard/me/ui-prefs" +TOUCHED = ("profile-collapsed", "profile-height", "b05-routeedit-cross-v2") +#: 구조물 형상 그림(관 · 박스 · 세월교 · 벽 · 맹암거) — 표시만이면 0 +SHAPES = ( + ".b06-chart__culvert-pipe, [class*='b06-chart__box'], [class*='b06-chart__ford']," + " [class*='b06-chart__wall'], [class*='underdrain']" +) + + +def _up() -> bool: + try: + urllib.request.urlopen(BASE, timeout=3) + return True + except Exception: + return False + + +pytestmark = pytest.mark.skipif(not _up(), reason="5179 서버 없음") + +JS_ADD = r"""async ([no, dist]) => { + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const sec = document.querySelector('.b05-structure-section'); + const fire = (el, v) => { el.value = v; el.dispatchEvent(new Event('change', { bubbles: true })); }; + const [group, type] = sec.querySelectorAll('.b05-structure__grid select'); + fire(group, 'A'); await w(300); + fire(type, 'cross_drain_exposed'); await w(500); + const [a, b] = sec.querySelectorAll('.b05-structure__position-row > label')[2].querySelectorAll('input'); + fire(a, String(no)); fire(b, String(dist)); await w(200); + sec.querySelector('.is-primary').click(); await w(1500); +}""" +JS_ITEMS = ( + "() => [...document.querySelectorAll('.b05-route__irregular-list li')]" + ".map((l) => l.textContent.trim())" +) +JS_SCROLL = r"""(m) => { + const marker = [...document.querySelectorAll('.b06-chart__station')] + .find((e) => Math.abs(+e.getAttribute('data-chainage') - m) < 1e-6); + const body = marker.closest('.b05-route-profile__body'); + body.scrollLeft = Math.max(0, +marker.querySelector('line').getAttribute('x1') - body.clientWidth / 2); +}""" +JS_POINT = r"""(m) => { + const marker = [...document.querySelectorAll('.b06-chart__station')] + .find((e) => Math.abs(+e.getAttribute('data-chainage') - m) < 1e-6); + const r = marker.querySelector('.b06-chart__station-line').getBoundingClientRect(); + return { x: r.left + r.width / 2, y: r.top + r.height * 0.3 }; +}""" +JS_CARD = r"""(scope) => { + const root = document.querySelector(scope); + const card = root?.querySelector('.b06-cross-card'); + return { + card: !!card, + chips: [...(root?.querySelectorAll('.b05-cross-chips > *') ?? [])].map((e) => e.textContent), + shapes: card ? card.querySelectorAll(SHAPES).length : -1, + }; +}""".replace("SHAPES", json.dumps(SHAPES)) +JS_B06 = r"""(c) => ({ + head: c.querySelector(':scope > header .b06-cross-card__structure')?.textContent ?? null, + label: c.querySelector('.b06-cross-card__title strong')?.textContent ?? null, + shapes: c.querySelectorAll(SHAPES).length, +})""".replace("SHAPES", json.dumps(SHAPES)) + + +@pytest.fixture(scope="module") +def page(): + sync = pytest.importorskip("playwright.sync_api") + cred = json.loads((ROOT / "tmp" / "aislo_cred.json").read_text(encoding="utf-8")) + with sync.sync_playwright() as play: + chrome = play.chromium.launch(channel="msedge", headless=True) + ctx = chrome.new_context(viewport={"width": 1600, "height": 1000}) + login = {k: cred[k] for k in ("email", "password")} + assert ctx.request.post(f"{BASE}/api/auth/login/request", data=login, timeout=60000).ok + prefs = ctx.request.get(f"{BASE}{PREFS}").json().get("prefs", {}) + tab = ctx.new_page() + tab.add_init_script(f"localStorage.setItem('frd_current_project_id', '{PROJECT}')") + tab.goto(f"{BASE}/#/b05-profile", wait_until="domcontentloaded") + tab.wait_for_function("() => !!window.__aisloPipeline", timeout=90000) + tab.wait_for_timeout(8000) + SHOTS.mkdir(parents=True, exist_ok=True) + try: + yield tab + finally: + tab.wait_for_timeout(2000) + now = ctx.request.get(f"{BASE}{PREFS}").json().get("prefs", {}) + for key in (f"aislo:pref:{name}" for name in TOUCHED): + if key in prefs: + now[key] = prefs[key] + else: + now.pop(key, None) + assert ctx.request.put(f"{BASE}{PREFS}", data={"prefs": now}).ok + chrome.close() + + +def _profile(page, open_: bool) -> None: + height = page.evaluate( + "() => document.querySelector('.b05-route-profile')?.getBoundingClientRect().height ?? 0" + ) + if (height > 100) != open_: + page.get_by_text("종단", exact=True).last.click() + page.wait_for_timeout(3000) + + +def test_1_추가_측점_섬(page): + before = page.evaluate(JS_ITEMS) + assert not any(NAME in t for t in before) + page.evaluate(JS_ADD, [AT_M // 20, AT_M % 20]) + page.wait_for_timeout(4000) + items = page.evaluate(JS_ITEMS) + assert f"7+7{NAME}" in items, items + assert len(items) == len(before) + 1 + + +def test_2_횡단_판_이름_칩_형상_0(page): + _profile(page, False) + page.locator(".b05-route__irregular-item", has_text=NAME).first.click() + page.wait_for_timeout(4000) + got = page.evaluate(JS_CARD, ".b05-routeedit__cross") + page.locator(".b05-routeedit__cross").first.screenshot(path=str(SHOTS / "panel.png")) + assert got["card"] and NAME in got["chips"] and got["shapes"] == 0, got + + +def test_3_임시_횡단_이름_칩_형상_0(page): + _profile(page, True) + page.evaluate(JS_SCROLL, AT_M) + page.wait_for_timeout(800) + at = page.evaluate(JS_POINT, AT_M) + page.mouse.move(at["x"], at["y"]) + page.wait_for_timeout(300) + page.mouse.click(at["x"], at["y"]) + page.wait_for_function( + "() => document.querySelector('.b05-crossmodal .b06-cross-card')", timeout=30000 + ) + page.wait_for_timeout(1500) + station = page.evaluate("() => document.querySelector('.b05-crossmodal__station').textContent") + got = page.evaluate(JS_CARD, ".b05-crossmodal") + page.locator(".b05-crossmodal").screenshot(path=str(SHOTS / "modal.png")) + page.locator(".b05-crossmodal").get_by_text("닫기").click() + assert station == "7+7" and NAME in got["chips"] and got["shapes"] == 0, (station, got) + + +def test_4_B06_카드_머리_이름_형상_0_지움(page): + page.evaluate("() => { location.hash = '#/b06-section'; }") + page.wait_for_timeout(9000) + page.locator(".b05-route__irregular-item:visible", has_text=NAME).first.click() + page.wait_for_timeout(3000) + card = page.locator(".b06-cross-card:visible", has_text=NAME).first + card.scroll_into_view_if_needed() + page.wait_for_timeout(1500) + got = card.evaluate(JS_B06) + card.screenshot(path=str(SHOTS / "b06_card.png")) + assert got == {"head": NAME, "label": "7+7", "shapes": 0}, got + # 되돌림 — 고른 채로 세션에서 지움([저장] 안 함) + section = page.locator(".b05-structure-section:visible").first + if not section.locator("button.is-danger", has_text="삭제").first.is_visible(): + page.get_by_text("구조물 배치", exact=True).first.click() # B06 = 칸이 접혀 있음 + page.wait_for_timeout(1500) + # 「구조물 전체 삭제」 와 헷갈리지 않게 글이 꼭 「삭제」 인 것만 + remove = section.get_by_role("button", name="삭제", exact=True) + if remove.is_disabled(): # 칸을 펴면 고름이 풀림 — 다시 고름 + page.locator(".b05-route__irregular-item:visible", has_text=NAME).first.click() + page.wait_for_timeout(1500) + remove.click() + page.wait_for_timeout(3000) + left = page.evaluate(JS_ITEMS) + page.screenshot(path=str(SHOTS / "b06_after_delete.png")) + assert not any(NAME in t for t in left), left From 0d4c3980217c76cc57a3113ea5613512e1b456ac Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 21 Oct 2026 15:54:26 +0900 Subject: [PATCH 3/5] =?UTF-8?q?chore(=EC=9A=94=EA=B5=AC):=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=20313=20=E2=80=94=20=EB=85=B8=EC=B6=9C=ED=98=95=20?= =?UTF-8?q?=ED=9A=A1=EB=8B=A8=EC=88=98=EB=A1=9C=20=C2=B7=20=EA=B0=9C?= =?UTF-8?q?=EA=B1=B0=20=3D=20=EC=B7=A8=EC=88=98=EC=A0=95=20=C2=B7=20?= =?UTF-8?q?=EA=B5=AD=EA=B0=80=EC=A7=80=EC=A0=90=EB=B2=88=ED=98=B8=ED=8C=90?= =?UTF-8?q?=EC=B2=98=EB=9F=BC(507-1=20=EB=90=98=EB=8F=8C=EB=A6=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/raw/요구_1021/사용자요구_1021.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/요구_1021/사용자요구_1021.md b/docs/raw/요구_1021/사용자요구_1021.md index 9858b60d4..6581e8648 100644 --- a/docs/raw/요구_1021/사용자요구_1021.md +++ b/docs/raw/요구_1021/사용자요구_1021.md @@ -26,3 +26,7 @@ 1. 계산위치 제어 페이지사 제어안됨? 추가로로직은 45개 아님? 2. (물음 답) 45줄 전체 표시 + +## 요구 313 (10-21 · 사용자 · 311-2 고침) + +1. 노출형 횡단수로와 개거에 대해 잘 못 지시함. 다른 구조물 중 취수정이나 국가지점 번호 처럼 동작해야함. 되돌리면 될것같음. 확인 From 59eda8ef0c21a8f3108801c0d8789005e69bf0bc Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 21 Oct 2026 15:55:01 +0900 Subject: [PATCH 4/5] =?UTF-8?q?Revert=20"fix(507-1=20=EB=85=B8=EC=B6=9C?= =?UTF-8?q?=ED=98=95=20=ED=9A=A1=EB=8B=A8=EC=88=98=EB=A1=9C=20=C2=B7=20?= =?UTF-8?q?=EA=B0=9C=EA=B1=B0=20=3D=20=ED=9A=A1=EB=8B=A8=EB=A9=B4=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=EB=A7=8C):=20=EB=B6=80=ED=92=88=20=EC=83=81?= =?UTF-8?q?=EC=9E=90=20=ED=98=95=EC=83=81=20=E3=80=8C=ED=91=9C=EC=8B=9C?= =?UTF-8?q?=EB=A7=8C=E3=80=8D(mark=20=C2=B7=20=EC=9D=BD=EB=8A=94=20?= =?UTF-8?q?=EC=B9=B8=20=EC=97=86=EC=9D=8C=20=C2=B7=20=EA=B7=B8=EB=A6=BC=20?= =?UTF-8?q?=EC=97=86=EC=9D=8C)=20=C2=B7=20=EB=91=90=20=EC=A2=85=EB=A5=98?= =?UTF-8?q?=20=EC=B2=98=EC=9D=8C=EA=B0=92=20=3D=20=EC=B8=A1=EC=A0=90=20?= =?UTF-8?q?=EC=84=B8=EC=9B=80(anchor)=20+=20=ED=91=9C=EC=8B=9C=EB=A7=8C=20?= =?UTF-8?q?=C2=B7=20B05=20=E3=80=8C=ED=9A=A1=EB=8B=A8=E3=80=8D=20=ED=8C=90?= =?UTF-8?q?=20=C2=B7=20=EC=9E=84=EC=8B=9C=20=ED=9A=A1=EB=8B=A8=20=EC=B0=BD?= =?UTF-8?q?=20=3D=20=EB=A8=B8=EB=A6=AC=20=EA=B1=B7=EA=B8=B0=20=EC=A0=84=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EC=9D=B4=EB=A6=84=20=EC=B9=A9=20?= =?UTF-8?q?=EB=82=A8=EA=B9=80=20=E2=80=94=20=EC=9A=94=EA=B5=AC=20311-2"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 254b5267fff3282df449a0e23854d4a892762497. --- .../B05_Profile_UI_RouteEdit_Cross_Card.ts | 6 +- common_util/common_util_structure_parts.json | 14 +- resources/tester/helper_322_2_check.cjs | 6 +- resources/tester/test_272_1_station_list.py | 4 +- resources/tester/test_322_1_catalog_mirror.py | 4 +- resources/tester/test_322_2_check_mirror.py | 10 +- resources/tester/test_322_3_plan_mirror.py | 4 +- .../tester/test_505_3_cross_card_screen.py | 4 +- .../tester/test_505_3_cross_card_text.py | 2 +- resources/tester/test_507_1_exposed_mark.py | 74 ------- .../tester/test_507_1_exposed_mark_screen.py | 189 ------------------ 11 files changed, 22 insertions(+), 295 deletions(-) delete mode 100644 resources/tester/test_507_1_exposed_mark.py delete mode 100644 resources/tester/test_507_1_exposed_mark_screen.py diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Cross_Card.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Cross_Card.ts index 4e28bcb5d..f99e4050e 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Cross_Card.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Cross_Card.ts @@ -156,10 +156,8 @@ export function routeCrossCard( box, ); // 그래프만(393-3) — 카드 머리 · 줌 단추 · 발(암 경계 · 요약) 걷음 · 벽 · 선 · 면적 고름 막음 - // 경고 칩(빈칸 · 기본값 · 사면 열림) · 구조물 이름(표시만 종류 · 507-1)만 그래프 오른쪽 위에 겹쳐 남김(505-3) - const chips = card.querySelectorAll( - ":scope > header .b06-cross-card__warning, :scope > header .b06-cross-card__structure", - ); + // 경고 칩(빈칸 · 기본값 · 사면 열림)만 그래프 오른쪽 위에 겹쳐 남김(505-3) + const chips = card.querySelectorAll(":scope > header .b06-cross-card__warning"); if (chips.length) { const box = document.createElement("div"); box.className = "b05-cross-chips"; diff --git a/common_util/common_util_structure_parts.json b/common_util/common_util_structure_parts.json index 1359dcc8e..aba3e422f 100644 --- a/common_util/common_util_structure_parts.json +++ b/common_util/common_util_structure_parts.json @@ -150,12 +150,6 @@ "읽는 칸": ["pave_spec", "height_m", "widening_off"], "자리꼴": ["interval"], "정본": ["structures"] - }, - "mark": { - "이름": "표시만(형상 없음 · 측점 · 이름만)", - "읽는 칸": [], - "자리꼴": ["point", "interval", "site"], - "정본": ["pipe_points", "structures"] } }, "3D": { @@ -229,16 +223,16 @@ }, "cross_drain_exposed": { "목록": "both", - "횡단도": "anchor", + "횡단도": "none", "카드이름": "name", - "형상": "mark", + "형상": "none", "3D": "none" }, "open_ditch": { "목록": "both", - "횡단도": "anchor", + "횡단도": "none", "카드이름": "name", - "형상": "mark", + "형상": "none", "3D": "none" }, "cross_underdrain": { diff --git a/resources/tester/helper_322_2_check.cjs b/resources/tester/helper_322_2_check.cjs index d0a4c02eb..5d2b7c70f 100644 --- a/resources/tester/helper_322_2_check.cjs +++ b/resources/tester/helper_322_2_check.cjs @@ -53,8 +53,8 @@ const enabled = (id) => def.types.find((t) => t.type_id === id).enabled; const own = (id) => clone(reflect.reflectOf(def)[id] ?? null); reflect.setReflect(def, "wildlife_path", "목록", "both"); reflect.setReflect(def, "retaining_wall", "목록", "b06"); -reflect.setReflect(def, "open_ditch", "횡단도", "anchor"); -reflect.setReflect(def, "cross_drain_exposed", "횡단도", "none"); +reflect.setReflect(def, "open_ditch", "횡단도", "none"); +reflect.setReflect(def, "cross_drain_exposed", "횡단도", "anchor"); const setBefore = reflect.isSet(def, "cross_drain_exposed", "횡단도"); reflect.setShapeOpt(def, "soil_guard", "weep", true); const weepOn = own("soil_guard"); @@ -95,8 +95,8 @@ const steps = []; reflect.setReflect(work, "masonry_wet", "목록", "both"); steps.push(reflect.differs(work, base)); reflect.setReflect(work, "masonry_wet", "목록", "hidden"); -reflect.setReflect(work, "open_ditch", "횡단도", "none"); reflect.setReflect(work, "open_ditch", "횡단도", "anchor"); +reflect.setReflect(work, "open_ditch", "횡단도", "none"); reflect.setShapeOpt(work, "soil_guard", "form", "콘크리트"); reflect.setShapeOpt(work, "soil_guard", "form", "통나무·목재틀"); reflect.setShown(work, "pipe", ["wing_wall_type"], false); diff --git a/resources/tester/test_272_1_station_list.py b/resources/tester/test_272_1_station_list.py index 686445bb1..845e8b0df 100644 --- a/resources/tester/test_272_1_station_list.py +++ b/resources/tester/test_272_1_station_list.py @@ -41,7 +41,7 @@ PIPES = [ }, ] STRUCTURES = [ - # 노출형 = 구조물 표 횡단도 anchor · 형상 표시만(507-1 · 요구 311-2) — 측점 심음 + # 노출형 = 구조물 표 횡단도 none(322-1 ★1) — 측점 안 심음 {"type_id": "cross_drain_exposed", "placement": "point", "chainage_m": 120.0}, { "type_id": "retaining_wall", @@ -67,7 +67,7 @@ STRUCTURES = [ }, {"type_id": "evac_road", "placement": "point", "chainage_m": 720.0}, ] -EXPECTED = [95, 120, 160, 230, 300, 395, 400, 410, 500, 510, 530, 545, 560] +EXPECTED = [95, 160, 230, 300, 395, 400, 410, 500, 510, 530, 545, 560] # 368-2 추가 측점 — 47 만 듦(95 관 · 300 세월교 자리는 그쪽이 이김) PLUS = [47.0, 95.0, 300.0] diff --git a/resources/tester/test_322_1_catalog_mirror.py b/resources/tester/test_322_1_catalog_mirror.py index acccaf659..906428a7b 100644 --- a/resources/tester/test_322_1_catalog_mirror.py +++ b/resources/tester/test_322_1_catalog_mirror.py @@ -38,8 +38,8 @@ TABLE = { "box_culvert": ("both", "anchor", "name", "box", "box"), "ford_pavement": ("both", "anchor", "name", "ford_pavement", "chute"), "ford_bridge": ("both", "anchor", "name", "ford", "ford"), - "cross_drain_exposed": ("both", "anchor", "name", "mark", "none"), # 표시만(요구 311-2 · 507-1) - "open_ditch": ("both", "anchor", "name", "mark", "none"), # 표시만(요구 311-2 · 507-1) + "cross_drain_exposed": ("both", "none", "name", "none", "none"), + "open_ditch": ("both", "none", "name", "none", "none"), "cross_underdrain": ("both", "anchor", "name", "underdrain", "surface"), **{ t: ("hidden", "none", "name", "none", "none") diff --git a/resources/tester/test_322_2_check_mirror.py b/resources/tester/test_322_2_check_mirror.py index 45e1d7f02..518b48d31 100644 --- a/resources/tester/test_322_2_check_mirror.py +++ b/resources/tester/test_322_2_check_mirror.py @@ -63,7 +63,7 @@ def _cases() -> dict[str, tuple[dict, dict | None]]: bad["부품"] = {"형상.zz": False, "횡단도.span3": "no", "끔": False} cases["shape_bad"] = (bad, real) rule = copy.deepcopy(real) - rule["반영"] = {"mirror": {"횡단도": "anchor"}} # 형상 없음(개거 = 표시만 · 507-1) + rule["반영"] = {"open_ditch": {"횡단도": "anchor"}} cases["rule218"] = (rule, real) mismatch = copy.deepcopy(real) mismatch["반영"] = {"retaining_wall": {"형상": "pipe_set"}, "berm": {"횡단도": "anchor"}} @@ -126,7 +126,7 @@ def test_check_rules(ts: dict) -> None: "부품 「횡단도.span3」 — 참거짓", "부품 「끔」 — 없는 부품", ] - assert got["rule218"] == ["「반사경」 형상이 「없음」이면 횡단도도 「안 만듦」"] + assert got["rule218"] == ["「개거(겉도랑)」 형상이 「없음」이면 횡단도도 「안 만듦」"] assert got["mismatch"] == ["「옹벽」 형상 「배관 세트」 — 구간 · 구조물에 안 맞음"] assert got["hide_read"] == [ "「배관」 형상 「배관 세트」 이 쓰는 칸 「흙막이 기초」 — 숨길 수 없음" @@ -158,8 +158,8 @@ def test_reflect_edits(ts: dict) -> None: r = ts["reflect"] assert r["wildlife"] == [None, True] # b06 → both = enabled 참이 곧 처음 값 짝(반영 안 남음) assert r["wall"] == [None, False] # both → b06 = enabled 거짓 - assert r["openDitch"] is None # 처음 값(anchor · 표시만 507-1)과 같음 — 반영 안 남음 - assert r["exposed"] == [{"횡단도": "none"}, True] + assert r["openDitch"] is None # 처음 값(none)과 같음 — 반영 안 남음 + assert r["exposed"] == [{"횡단도": "anchor"}, True] assert r["weepOn"] == {"형상칸": {"weep": True}} and r["weepOff"] is None assert r["shown"] == {"pipe_kind": False, "inlet_type": False} # 정의 옵션만 assert r["readPipe"] == ["inlet_basin_height_m", "revet_foundation"] # 기본값 없는 읽는 칸 @@ -233,7 +233,7 @@ def test_server_save_reflect_parts_color_enabled(copy_def: Path) -> None: @pytest.mark.parametrize( "change", [ - lambda d: d.update(반영={"mirror": {"횡단도": "anchor"}}), # 218 + lambda d: d.update(반영={"open_ditch": {"횡단도": "anchor"}}), # 218 lambda d: d["표시"].setdefault("pipe", {}).update(revet_foundation=False), # 못 숨김 칸 lambda d: next(o for o in _type(d, "pipe")["options"] if o["key"] == "pipe_kind")[ "choices" diff --git a/resources/tester/test_322_3_plan_mirror.py b/resources/tester/test_322_3_plan_mirror.py index 09cd2abbb..6854ae3ea 100644 --- a/resources/tester/test_322_3_plan_mirror.py +++ b/resources/tester/test_322_3_plan_mirror.py @@ -147,7 +147,6 @@ def test_plan_rules(ts: dict) -> None: mixed = ts["mixed"] assert [round(s["chainage_m"], 3) for s in mixed] == [ 95.0, - 120.0, # 노출형 횡단수로 = 표시만 측점(507-1) 160.0, 230.0, 395.0, @@ -165,8 +164,7 @@ def test_plan_rules(ts: dict) -> None: assert first["blank"] == ["pipe_diameter_mm", "culvert_height_m"] joint = next(s for s in mixed if s["chainage_m"] == 530.0) # 옹벽 끝 = 소단 시작 assert joint["owners"] == ["s2", "s3"] and joint["title"] == "옹벽" and joint["role"] == "end" - exposed = next(s for s in mixed if s["chainage_m"] == 120.0) # 표시만 = 이름 · 빈칸 없음(507-1) - assert exposed["title"] == "노출형 횡단수로" and exposed["blank"] == [] + assert all(s["title"] != "노출형 횡단수로" for s in mixed) # ★1 횡단도 none kinds = ts["pipe_kinds"] assert [s["title"] for s in kinds] == ["흄관 D800", "파형강관 D1000", "세월교"] assert kinds[1]["blank"] == ["pipe_diameter_mm"] and kinds[0]["blank"] == [] diff --git a/resources/tester/test_505_3_cross_card_screen.py b/resources/tester/test_505_3_cross_card_screen.py index 872ef6687..61dd405b5 100644 --- a/resources/tester/test_505_3_cross_card_screen.py +++ b/resources/tester/test_505_3_cross_card_screen.py @@ -175,7 +175,7 @@ def test_0_곡선_판_도구줄_횡단판_비킴(page): def test_1_빈칸_칩_카드(page): modal = _open_modal(page) assert modal["station"] == "19+8" and modal["card"] - assert modal["chips"] == ["⚠ 배관 관경 빈칸", "배수관"] # 이름 칩(507-1) + assert modal["chips"] == ["⚠ 배관 관경 빈칸"] assert modal["pipes"] == 0 and modal["width"] == 380 assert all("ui-btn ui-btn--ghost ui-btn--sm" in name for name in modal["buttons"]) page.locator(".b05-crossmodal").get_by_text("닫기").click() @@ -198,7 +198,7 @@ def test_2_횡단_판_칩_관경_넣으면_형상_확대_밝음_어두움(page): ).first.select_option("800") # 세션 초안만 — [저장] 안 누름 page.wait_for_timeout(5000) modal = _open_modal(page) - assert modal["chips"] == ["배수관"] and modal["pipes"] > 0 + assert modal["chips"] == [] and modal["pipes"] > 0 shot = page.locator(".b05-crossmodal") shot.screenshot(path=str(SHOTS / "modal_light.png")) page.evaluate("document.documentElement.setAttribute('data-theme', 'dark')") diff --git a/resources/tester/test_505_3_cross_card_text.py b/resources/tester/test_505_3_cross_card_text.py index 25e350e53..05ecc02e1 100644 --- a/resources/tester/test_505_3_cross_card_text.py +++ b/resources/tester/test_505_3_cross_card_text.py @@ -47,7 +47,7 @@ def test_판_재료_한_벌() -> None: def test_빈칸_칩은_머리_걷기_전에_꺼냄() -> None: card = _read("B05_Profile", "B05_Profile_UI_RouteEdit_Cross_Card.ts") - chips = card.index('":scope > header .b06-cross-card__warning') # 이름 칩도 같이(507-1) + chips = card.index('":scope > header .b06-cross-card__warning"') remove = card.index(':scope > header, :scope > footer, .b06-cross-card__zoom"') assert chips < remove assert 'box.className = "b05-cross-chips";' in card diff --git a/resources/tester/test_507_1_exposed_mark.py b/resources/tester/test_507_1_exposed_mark.py deleted file mode 100644 index 797c0a7b1..000000000 --- a/resources/tester/test_507_1_exposed_mark.py +++ /dev/null @@ -1,74 +0,0 @@ -"""PLAN 507-1 노출형 횡단수로 · 개거 = 횡단면 표시만(요구 311-2). - -- 부품 상자 형상 `mark`(표시만) — 읽는 칸 · 필수 칸 없음 → 빈칸 경고 없음 · 그리는 함수 없음. -- 처음값 = 횡단도 anchor(측점 세움) · 카드이름 name · 형상 mark · 3D none. -- 측점 계획 = 그 자리에 측점 하나 · 이름 = 종류 이름. -- 실제 정의(사용자 마스터 · 반영 없음) = M02 검사 오류 없음(218 규칙 = 형상 none 만 막음). -- B05 「횡단」 판 · 임시 횡단 창 = 카드 머리를 걷기 전에 구조물 이름 칩을 꺼내 남김. -""" - -from __future__ import annotations - -from pathlib import Path - -from B05_Profile.B05_Profile_Structures_Schema import StructureInstance, load_definition -from common_util.common_util_structure_catalog import ( - catalog, - load_parts, - resolve_catalog, - shape_needs, -) -from common_util.common_util_structure_station import plan_stations -from M02_MasterTemplete.StructOptions.M02_SO_Check import catalog_errors - -ROOT = Path(__file__).resolve().parents[2] -KINDS = {"cross_drain_exposed": "노출형 횡단수로", "open_ditch": "개거(겉도랑)"} - - -def test_mark_part_has_no_fields() -> None: - mark = load_parts()["parts"]["형상"]["mark"] - assert mark["읽는 칸"] == [] and "필수 칸" not in mark - assert "point" in mark["자리꼴"] and "structures" in mark["정본"] - - -def test_start_values_are_mark_only() -> None: - rows = resolve_catalog(load_definition()) - for type_id in KINDS: - row = rows[type_id] - got = (row["station"], row["title"], row["shape"], row["model3d"]) - assert got == ("anchor", "name", "mark", "none"), type_id - assert shape_needs(type_id) == [] - - -def test_station_planted_with_name() -> None: - structures = [ - StructureInstance.model_validate( - { - "structure_id": f"s{i}", - "type_id": type_id, - "placement": "point", - "chainage_m": at, - "options": {}, - } - ) - for i, (type_id, at) in enumerate((("cross_drain_exposed", 120.0), ("open_ditch", 240.0))) - ] - plan = plan_stations([], structures) - assert [(round(s["chainage_m"], 3), s["title"], s["blank"]) for s in plan] == [ - (120.0, KINDS["cross_drain_exposed"], []), - (240.0, KINDS["open_ditch"], []), - ] - - -def test_real_definition_passes_m02_check() -> None: - assert load_definition().get("반영") == {} # 사용자 마스터 값은 안 고침 - assert catalog_errors(load_definition(), None) == [] - assert all(catalog()[t]["shape"] == "mark" for t in KINDS) - - -def test_b05_card_keeps_structure_name_chip() -> None: - card = (ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit_Cross_Card.ts").read_text( - encoding="utf-8" - ) - keep = card.index(":scope > header .b06-cross-card__structure") - assert keep < card.index(':scope > header, :scope > footer, .b06-cross-card__zoom"') diff --git a/resources/tester/test_507_1_exposed_mark_screen.py b/resources/tester/test_507_1_exposed_mark_screen.py deleted file mode 100644 index 5a22d3a3a..000000000 --- a/resources/tester/test_507_1_exposed_mark_screen.py +++ /dev/null @@ -1,189 +0,0 @@ -"""PLAN 507-1 화면 — 자기 포트(5179) Playwright 머리 없는 브라우저 · 서버가 떠 있을 때만 · 검증_sub4. - -- B05 「구조물 배치」 노출형 횡단수로 7+7(147 m) 추가(세션만 · [저장] 안 함) → 측점 목록에 섬 -- 「횡단」 판 · 임시 횡단 창 = B06 카드 + 이름 칩 「노출형 횡단수로」 · 구조물 형상 0 -- B06 횡단 카드 머리 = 이름 · 형상 0 → 지움(세션) · 측점 목록에서 빠짐 -- 계정 화면 취향만 시험 앞 값으로 되돌림 · DB · storage 안 씀 - - ./venv/Scripts/python.exe -m pytest resources/tester/test_507_1_exposed_mark_screen.py -q -""" - -from __future__ import annotations - -import json -import urllib.request -from pathlib import Path - -import pytest - -ROOT = Path(__file__).resolve().parents[2] -BASE = "http://localhost:5179" -PROJECT = "c725d747-50ae-46b0-87ce-4d4e56a53cf3" # 검증_sub4(OWNERS.md) -SHOTS = ROOT / "tmp" / "shots_507_1" -AT_M = 147 # 7+7 — 격자 · 관(19+8) 측점과 안 겹침 -NAME = "노출형 횡단수로" -PREFS = "/api/dashboard/me/ui-prefs" -TOUCHED = ("profile-collapsed", "profile-height", "b05-routeedit-cross-v2") -#: 구조물 형상 그림(관 · 박스 · 세월교 · 벽 · 맹암거) — 표시만이면 0 -SHAPES = ( - ".b06-chart__culvert-pipe, [class*='b06-chart__box'], [class*='b06-chart__ford']," - " [class*='b06-chart__wall'], [class*='underdrain']" -) - - -def _up() -> bool: - try: - urllib.request.urlopen(BASE, timeout=3) - return True - except Exception: - return False - - -pytestmark = pytest.mark.skipif(not _up(), reason="5179 서버 없음") - -JS_ADD = r"""async ([no, dist]) => { - const w = (ms) => new Promise((r) => setTimeout(r, ms)); - const sec = document.querySelector('.b05-structure-section'); - const fire = (el, v) => { el.value = v; el.dispatchEvent(new Event('change', { bubbles: true })); }; - const [group, type] = sec.querySelectorAll('.b05-structure__grid select'); - fire(group, 'A'); await w(300); - fire(type, 'cross_drain_exposed'); await w(500); - const [a, b] = sec.querySelectorAll('.b05-structure__position-row > label')[2].querySelectorAll('input'); - fire(a, String(no)); fire(b, String(dist)); await w(200); - sec.querySelector('.is-primary').click(); await w(1500); -}""" -JS_ITEMS = ( - "() => [...document.querySelectorAll('.b05-route__irregular-list li')]" - ".map((l) => l.textContent.trim())" -) -JS_SCROLL = r"""(m) => { - const marker = [...document.querySelectorAll('.b06-chart__station')] - .find((e) => Math.abs(+e.getAttribute('data-chainage') - m) < 1e-6); - const body = marker.closest('.b05-route-profile__body'); - body.scrollLeft = Math.max(0, +marker.querySelector('line').getAttribute('x1') - body.clientWidth / 2); -}""" -JS_POINT = r"""(m) => { - const marker = [...document.querySelectorAll('.b06-chart__station')] - .find((e) => Math.abs(+e.getAttribute('data-chainage') - m) < 1e-6); - const r = marker.querySelector('.b06-chart__station-line').getBoundingClientRect(); - return { x: r.left + r.width / 2, y: r.top + r.height * 0.3 }; -}""" -JS_CARD = r"""(scope) => { - const root = document.querySelector(scope); - const card = root?.querySelector('.b06-cross-card'); - return { - card: !!card, - chips: [...(root?.querySelectorAll('.b05-cross-chips > *') ?? [])].map((e) => e.textContent), - shapes: card ? card.querySelectorAll(SHAPES).length : -1, - }; -}""".replace("SHAPES", json.dumps(SHAPES)) -JS_B06 = r"""(c) => ({ - head: c.querySelector(':scope > header .b06-cross-card__structure')?.textContent ?? null, - label: c.querySelector('.b06-cross-card__title strong')?.textContent ?? null, - shapes: c.querySelectorAll(SHAPES).length, -})""".replace("SHAPES", json.dumps(SHAPES)) - - -@pytest.fixture(scope="module") -def page(): - sync = pytest.importorskip("playwright.sync_api") - cred = json.loads((ROOT / "tmp" / "aislo_cred.json").read_text(encoding="utf-8")) - with sync.sync_playwright() as play: - chrome = play.chromium.launch(channel="msedge", headless=True) - ctx = chrome.new_context(viewport={"width": 1600, "height": 1000}) - login = {k: cred[k] for k in ("email", "password")} - assert ctx.request.post(f"{BASE}/api/auth/login/request", data=login, timeout=60000).ok - prefs = ctx.request.get(f"{BASE}{PREFS}").json().get("prefs", {}) - tab = ctx.new_page() - tab.add_init_script(f"localStorage.setItem('frd_current_project_id', '{PROJECT}')") - tab.goto(f"{BASE}/#/b05-profile", wait_until="domcontentloaded") - tab.wait_for_function("() => !!window.__aisloPipeline", timeout=90000) - tab.wait_for_timeout(8000) - SHOTS.mkdir(parents=True, exist_ok=True) - try: - yield tab - finally: - tab.wait_for_timeout(2000) - now = ctx.request.get(f"{BASE}{PREFS}").json().get("prefs", {}) - for key in (f"aislo:pref:{name}" for name in TOUCHED): - if key in prefs: - now[key] = prefs[key] - else: - now.pop(key, None) - assert ctx.request.put(f"{BASE}{PREFS}", data={"prefs": now}).ok - chrome.close() - - -def _profile(page, open_: bool) -> None: - height = page.evaluate( - "() => document.querySelector('.b05-route-profile')?.getBoundingClientRect().height ?? 0" - ) - if (height > 100) != open_: - page.get_by_text("종단", exact=True).last.click() - page.wait_for_timeout(3000) - - -def test_1_추가_측점_섬(page): - before = page.evaluate(JS_ITEMS) - assert not any(NAME in t for t in before) - page.evaluate(JS_ADD, [AT_M // 20, AT_M % 20]) - page.wait_for_timeout(4000) - items = page.evaluate(JS_ITEMS) - assert f"7+7{NAME}" in items, items - assert len(items) == len(before) + 1 - - -def test_2_횡단_판_이름_칩_형상_0(page): - _profile(page, False) - page.locator(".b05-route__irregular-item", has_text=NAME).first.click() - page.wait_for_timeout(4000) - got = page.evaluate(JS_CARD, ".b05-routeedit__cross") - page.locator(".b05-routeedit__cross").first.screenshot(path=str(SHOTS / "panel.png")) - assert got["card"] and NAME in got["chips"] and got["shapes"] == 0, got - - -def test_3_임시_횡단_이름_칩_형상_0(page): - _profile(page, True) - page.evaluate(JS_SCROLL, AT_M) - page.wait_for_timeout(800) - at = page.evaluate(JS_POINT, AT_M) - page.mouse.move(at["x"], at["y"]) - page.wait_for_timeout(300) - page.mouse.click(at["x"], at["y"]) - page.wait_for_function( - "() => document.querySelector('.b05-crossmodal .b06-cross-card')", timeout=30000 - ) - page.wait_for_timeout(1500) - station = page.evaluate("() => document.querySelector('.b05-crossmodal__station').textContent") - got = page.evaluate(JS_CARD, ".b05-crossmodal") - page.locator(".b05-crossmodal").screenshot(path=str(SHOTS / "modal.png")) - page.locator(".b05-crossmodal").get_by_text("닫기").click() - assert station == "7+7" and NAME in got["chips"] and got["shapes"] == 0, (station, got) - - -def test_4_B06_카드_머리_이름_형상_0_지움(page): - page.evaluate("() => { location.hash = '#/b06-section'; }") - page.wait_for_timeout(9000) - page.locator(".b05-route__irregular-item:visible", has_text=NAME).first.click() - page.wait_for_timeout(3000) - card = page.locator(".b06-cross-card:visible", has_text=NAME).first - card.scroll_into_view_if_needed() - page.wait_for_timeout(1500) - got = card.evaluate(JS_B06) - card.screenshot(path=str(SHOTS / "b06_card.png")) - assert got == {"head": NAME, "label": "7+7", "shapes": 0}, got - # 되돌림 — 고른 채로 세션에서 지움([저장] 안 함) - section = page.locator(".b05-structure-section:visible").first - if not section.locator("button.is-danger", has_text="삭제").first.is_visible(): - page.get_by_text("구조물 배치", exact=True).first.click() # B06 = 칸이 접혀 있음 - page.wait_for_timeout(1500) - # 「구조물 전체 삭제」 와 헷갈리지 않게 글이 꼭 「삭제」 인 것만 - remove = section.get_by_role("button", name="삭제", exact=True) - if remove.is_disabled(): # 칸을 펴면 고름이 풀림 — 다시 고름 - page.locator(".b05-route__irregular-item:visible", has_text=NAME).first.click() - page.wait_for_timeout(1500) - remove.click() - page.wait_for_timeout(3000) - left = page.evaluate(JS_ITEMS) - page.screenshot(path=str(SHOTS / "b06_after_delete.png")) - assert not any(NAME in t for t in left), left From 1a8a16a99b8c42abdbe1e6315e9acea5ad8e93e5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 21 Oct 2026 16:03:02 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix(507-3=20MC=20=EA=B0=95=EC=A1=B0):=20B05?= =?UTF-8?q?=20=C2=B7=20B06=20MC=20=EC=B8=A1=EC=A0=90=20=EA=B8=80=20(MC)=20?= =?UTF-8?q?=C2=B7=20=ED=95=9C=20=EB=8B=A8=EA=B3=84=20=ED=81=AC=EA=B2=8C=20?= =?UTF-8?q?=C2=B7=20=EA=B5=B5=EA=B2=8C=20=C2=B7=20=EA=B2=BD=EA=B3=A0?= =?UTF-8?q?=EC=83=89=20=C2=B7=20B07=20=EC=A2=85=EB=8B=A8=20=ED=91=9C=20?= =?UTF-8?q?=C2=B7=20=ED=9A=A1=EB=8B=A8=20=EC=A0=9C=EB=AA=A9=20=C2=B7=20B08?= =?UTF-8?q?=20=ED=86=A0=EC=A0=81=ED=91=9C=20=C2=B7=20B09=20=EC=9D=B8?= =?UTF-8?q?=EC=87=84=20=ED=86=A0=EC=A0=81=ED=91=9C=20(MC)=20=E2=80=94=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=20311-4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 평면 눈금 굵은 13px · 종단 12px · 횡단 판 · B06 카드 · 상세 창 제목 = --color-warning(밝음 · 어두움) - 측점 갈아끼움 이름 = 서버 꼴 STA… (MC) - B08 토적표 줄 mc = 서버가 종단 파일 kind curve 로 실음 - test_507_3_mc_label Co-Authored-By: Claude Opus 5.5 --- .../B04_PreProcess_UI_MapOverlays.ts | 6 +- B05_Profile/B05_Profile_Api_Replan.ts | 2 + B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts | 5 +- .../B05_Profile_UI_Style_RouteEdit.css | 7 ++ .../B06_Section_UI_Cross_Card_Chrome.ts | 1 + B06_Section/B06_Section_UI_Cross_Detail.ts | 5 +- B06_Section/B06_Section_UI_Longitudinal.ts | 5 +- B06_Section/B06_Section_UI_Style_Cross.css | 19 ++++ B06_Section/logic/B06_Section_Station_Swap.ts | 2 +- .../B07_DesignDetail_Engine_Cad.py | 5 ++ .../B07_DesignDetail_Engine_Cad_Long.py | 3 +- .../B07_DesignDetail_Router_Support.py | 6 +- B08_Quantity/B08_Quantity_Earthwork_Tables.ts | 7 +- B08_Quantity/B08_Quantity_Router_Earthwork.py | 25 ++++++ resources/tester/test_507_3_mc_label.py | 89 +++++++++++++++++++ 15 files changed, 176 insertions(+), 11 deletions(-) create mode 100644 resources/tester/test_507_3_mc_label.py diff --git a/B04_PreProcess/B04_PreProcess_UI_MapOverlays.ts b/B04_PreProcess/B04_PreProcess_UI_MapOverlays.ts index 2f86f503d..c0b468e71 100644 --- a/B04_PreProcess/B04_PreProcess_UI_MapOverlays.ts +++ b/B04_PreProcess/B04_PreProcess_UI_MapOverlays.ts @@ -292,6 +292,8 @@ export function drawStationTicks( context.stroke(); const label = stationLabel(chainage, interval, options.chainages ? 1 : 0) + (mc ? " (MC)" : ""); + // MC = 사용자가 고칠 자리(507-3) — 한 단계 큰 굵은 글 · 경고색 · 바탕 = 테마 바탕(밝음 · 어두움 둘 다 읽힘) + context.font = mc ? "bold 13px system-ui, sans-serif" : "11px system-ui, sans-serif"; const width = context.measureText(label).width + 6; const half = width / 2; // 겹치면 빼지 않고 **비켜 놓는다** — 직각 방향으로 반대쪽 → 한 칸 더 멀리 순서로 빈자리를 @@ -328,9 +330,9 @@ export function drawStationTicks( context.translate(-lx, -ly); } // 배경을 깔아 등고선 위에서도 읽히게 한다. - context.fillStyle = "rgba(255, 255, 255, 0.78)"; + context.fillStyle = mc ? themeColor("--color-canvas", "#ffffff") : "rgba(255, 255, 255, 0.78)"; context.fillRect(lx - half, ly - 8, width, 16); - context.fillStyle = "#222222"; + context.fillStyle = mc ? themeColor("--color-warning", "#b8860b") : "#222222"; context.fillText(label, lx, ly); context.restore(); } diff --git a/B05_Profile/B05_Profile_Api_Replan.ts b/B05_Profile/B05_Profile_Api_Replan.ts index 5c773c27a..e34cec101 100644 --- a/B05_Profile/B05_Profile_Api_Replan.ts +++ b/B05_Profile/B05_Profile_Api_Replan.ts @@ -242,6 +242,8 @@ export interface CrossPreviewResponse { status: string; chainage_m: number; label: string | null; + /** 측점 갈래 — `curve` = MC(이름 뒤 「 (MC)」 · 507-3). */ + kind?: string | null; uphill_side: string | null; plan_radius_m: number | null; curve_widening_m: number | null; diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts index 07faa1632..6b637970b 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts @@ -197,7 +197,9 @@ function createPane(title: string, tag: string, empty: string): CrossPane { } // 측점은 **누가거리가 아니라 측점 표기**로 낸다(계획서 0-9 ㉑) — B05 왼쪽 아래 구조물 // 목록이 쓰는 그 규칙이다. 서버가 주는 `STA.0+100.000` 을 그대로 쓰면 표기가 갈린다. - station.textContent = `${tag}${formatStation(preview.chainage_m, intervalM)}`; + const mc = preview.kind === "curve"; // MC = 고칠 자리(507-3) — (MC) · 크게 · 굵게 · 경고색 + station.textContent = `${tag}${formatStation(preview.chainage_m, intervalM)}${mc ? " (MC)" : ""}`; + station.className = `b05-routeedit__cross-station${mc ? " b06-station--mc" : ""}`; draw(preview); foot.textContent = source ? "" : summarizeCross(preview); // 카드 = 그래프만(393-3) }, @@ -228,6 +230,7 @@ export function pipelineCrossAt( status: "success", chainage_m: section.chainage_m, label: section.label ?? null, + kind: section.kind ?? null, uphill_side: section.uphill_side ?? null, plan_radius_m: section.plan_radius_m ?? null, curve_widening_m: section.curve_widening_m ?? null, diff --git a/B05_Profile/B05_Profile_UI_Style_RouteEdit.css b/B05_Profile/B05_Profile_UI_Style_RouteEdit.css index 4326777db..0c674c595 100644 --- a/B05_Profile/B05_Profile_UI_Style_RouteEdit.css +++ b/B05_Profile/B05_Profile_UI_Style_RouteEdit.css @@ -259,6 +259,13 @@ pointer-events: none; } +/* MC 측점(507-3) — 한 단계 크게 · 굵게 · 경고색 */ +.b05-routeedit__cross-station.b06-station--mc { + font-size: var(--text-body-sm); + font-weight: var(--font-weight-bold); + color: var(--color-warning); +} + .b05-routeedit__cross-station:empty { display: none; } diff --git a/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts b/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts index 5426239be..9c3e0e531 100644 --- a/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts +++ b/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts @@ -89,6 +89,7 @@ export function appendCardHeader( const label = document.createElement("strong"); label.textContent = stationLabel(section.chainage_m, stationInterval) + (section.kind === "curve" ? " (MC)" : ""); + if (section.kind === "curve") label.className = "b06-station--mc"; // 507-3 크게 · 굵게 · 경고색 const chainage = document.createElement("span"); chainage.textContent = `${section.chainage_m.toFixed(1)}m`; title.append(label, chainage); diff --git a/B06_Section/B06_Section_UI_Cross_Detail.ts b/B06_Section/B06_Section_UI_Cross_Detail.ts index d5aee6fbc..d15697bb7 100644 --- a/B06_Section/B06_Section_UI_Cross_Detail.ts +++ b/B06_Section/B06_Section_UI_Cross_Detail.ts @@ -298,7 +298,10 @@ function drawChart( function titleOf(section: CrossSection): string { const stations = host?.detail()?.longitudinal.stations ?? []; - return stationLabel(section.chainage_m, inferStationInterval(stations)); + return ( + stationLabel(section.chainage_m, inferStationInterval(stations)) + + (section.kind === "curve" ? " (MC)" : "") + ); } /** [상세] — 이미 열린 것은 닫고 이 측점으로 엶. */ diff --git a/B06_Section/B06_Section_UI_Longitudinal.ts b/B06_Section/B06_Section_UI_Longitudinal.ts index d24b60047..6c5dd3dcc 100644 --- a/B06_Section/B06_Section_UI_Longitudinal.ts +++ b/B06_Section/B06_Section_UI_Longitudinal.ts @@ -404,7 +404,7 @@ export function createLongitudinalProfile( const stationTiers = labelTiers( data.stations.map((station, index) => ({ x: x(station.chainage_m), - width: labelWidthPx(stationTexts[index], 10), + width: labelWidthPx(stationTexts[index], data.stations[index].kind === "curve" ? 12 : 10), })), ); const named = data.stations.filter((station) => station.structure); @@ -424,7 +424,8 @@ export function createLongitudinalProfile( x: stationX, y: heightPx - bottomInsetPx - 5 - LABEL_TIER_PX * tier, "text-anchor": "middle", - class: `b06-chart__station-label${tier ? " b06-chart__station-label--lifted" : ""}${tier && selected ? " b06-chart__station-label--selected" : ""}`, + // MC = 사용자가 고칠 자리(507-3) — 크게 · 굵게 · 경고색(_Style_Cross.css) + class: `b06-chart__station-label${station.kind === "curve" ? " b06-chart__station-label--mc" : ""}${tier ? " b06-chart__station-label--lifted" : ""}${tier && selected ? " b06-chart__station-label--selected" : ""}`, }); if (tier) structureLabels.push(label); const marker = svgElement("g", { diff --git a/B06_Section/B06_Section_UI_Style_Cross.css b/B06_Section/B06_Section_UI_Style_Cross.css index bf1a2099b..1ac1dd830 100644 --- a/B06_Section/B06_Section_UI_Style_Cross.css +++ b/B06_Section/B06_Section_UI_Style_Cross.css @@ -332,6 +332,12 @@ html.is-bm-route .b06-section__notice-bar { font-size: var(--text-body-sm); } +/* MC 측점 이름(507-3) — 카드 머리 글보다 한 단계 크게 · 경고색 */ +.b06-cross-card > header strong.b06-station--mc { + color: var(--color-warning); + font-size: var(--text-body); +} + /* 비정규 측점의 구조물 라벨 — kind(일반측점/BP/EP) 좌측에 자수정색 pill로 표기. */ .b06-cross-card__structure { padding: 1px 6px; @@ -395,6 +401,19 @@ html.is-bm-route .b06-section__notice-bar { font-size: 10px; } +/* MC(곡선 가운데) 측점 = 사용자가 고칠 자리(507-3) — 한 단계 크게 · 굵게 · 경고색. 고름 빨강은 아래가 이김. */ +.b06-chart__station-label--mc { + font-size: 12px; + font-weight: var(--font-weight-bold); + fill: var(--color-warning); +} + +.b06-station--mc { + font-size: 1.15em; + font-weight: var(--font-weight-bold); + color: var(--color-warning); +} + /* 중심고 Y축 라벨(318-2) — 눈금과 같은 10px · 둥근 상자 · 축선 앞에서 멈춤 · 바탕 = 불투명 섞음(겹친 눈금 글을 위에서 가림 · 319-1) · 색 = 원지반선 · 설계선(379-3 · `--b06-tag` = _Cross_Areas.css) */ .b06-chart__center-box { diff --git a/B06_Section/logic/B06_Section_Station_Swap.ts b/B06_Section/logic/B06_Section_Station_Swap.ts index d17f03c52..128223999 100644 --- a/B06_Section/logic/B06_Section_Station_Swap.ts +++ b/B06_Section/logic/B06_Section_Station_Swap.ts @@ -292,7 +292,7 @@ export function swapCrossSections( const place = { station_id: prior && !vacated ? prior.station_id : stationIdOf(station.chainageM), chainage_m: station.chainageM, - label: formatStation(station.chainageM), + label: formatStation(station.chainageM) + (kind === "curve" ? " (MC)" : ""), // 서버 꼴(507-3) kind, ...(onGrid ? { grid: true } : {}), center_z: canon diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Cad.py b/B07_DesignDetail/B07_DesignDetail_Engine_Cad.py index 096427eb8..f3c623d9c 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Cad.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Cad.py @@ -330,6 +330,11 @@ def station_no_label(chainage_m: float, interval_m: float) -> str: return f"No.{number}+{remainder:.1f}" +def mc_suffix(station: dict[str, Any] | None) -> str: + """곡선 가운데(MC · `kind` curve) 측점 글 뒤 「 (MC)」 — 09-03 표기 위에 더함(507-3).""" + return " (MC)" if isinstance(station, dict) and station.get("kind") == "curve" else "" + + def _profile_alignment(longitudinal: dict[str, Any]) -> dict[str, Any]: alignment = longitudinal.get("profile_alignment") return alignment if isinstance(alignment, dict) else {} diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Long.py b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Long.py index e52e08bc8..1a889d09a 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Long.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Long.py @@ -37,6 +37,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import ( _stations, _text_entity, infer_station_interval, + mc_suffix, points_from_samples, polyline_entity, station_no_label, @@ -155,7 +156,7 @@ def _long_table_values( distance = chainage - chainage_all[position - 1] if position > 0 else None rows.append( { - "station": station_no_label(chainage, interval_m), + "station": station_no_label(chainage, interval_m) + mc_suffix(station), "distance": _format(distance, 1), "cum_distance": _format(chainage, 1), "ground": _format(ground), diff --git a/B07_DesignDetail/B07_DesignDetail_Router_Support.py b/B07_DesignDetail/B07_DesignDetail_Router_Support.py index 676d21211..1f61b6729 100644 --- a/B07_DesignDetail/B07_DesignDetail_Router_Support.py +++ b/B07_DesignDetail/B07_DesignDetail_Router_Support.py @@ -21,6 +21,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import ( GROUND_LAYER_ID, build_cross_drawing, infer_station_interval, + mc_suffix, station_no_label, ) from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Basin import build_watershed_drawing @@ -426,7 +427,8 @@ def _cross_section_input( ), "design_line": _cross_design_line(longitudinal_path, source, design), "quantity_table": _quantity_table(source, design), - "title": station_no_label(float(source.get("chainage_m", chainage)), interval), + "title": station_no_label(float(source.get("chainage_m", chainage)), interval) + + mc_suffix(source), "overlay": overlay, } @@ -616,7 +618,7 @@ def _read_drawing( # 수량표 제목행 No. 표기: 종단 측점 간격 기준 (납품 도면 양식) longitudinal = _read_json(longitudinal_path) interval = infer_station_interval(longitudinal.get("stations") or []) - title = station_no_label(float(source.get("chainage_m", 0.0)), interval) + title = station_no_label(float(source.get("chainage_m", 0.0)), interval) + mc_suffix(source) # 계획고(로컬좌표 기준) + 노선 공통 범위 — 측점 이동 시 화면 배치 고정. design_elevation = design_elevation_from_longitudinal( longitudinal, float(source.get("chainage_m", 0.0)) diff --git a/B08_Quantity/B08_Quantity_Earthwork_Tables.ts b/B08_Quantity/B08_Quantity_Earthwork_Tables.ts index efa789d0e..cb3202ffc 100644 --- a/B08_Quantity/B08_Quantity_Earthwork_Tables.ts +++ b/B08_Quantity/B08_Quantity_Earthwork_Tables.ts @@ -48,6 +48,8 @@ interface EarthworkRow { cumulative_m3: number; notes: string[]; faces: Record; + /** MC(곡선 가운데) 측점 — 측점 글 뒤 「 (MC)」(507-3 · 서버 종단 `kind` curve). */ + mc?: boolean; } export interface HaulRow { @@ -184,7 +186,10 @@ function stationRow(row: EarthworkRow, first: boolean, interval: number): DataRo // 첫 측점은 앞이 없어 입적 · 보정량 · 면적이 없음(빈 값) const v = (value: number): number | null => (first ? null : value); const out: DataRow = { - station: stationLabel(row.chainage_m, interval) + (row.notes.length ? " *" : ""), + station: + stationLabel(row.chainage_m, interval) + + (row.mc ? " (MC)" : "") + + (row.notes.length ? " *" : ""), distance: row.distance_m, adjusted_total: v(row.adjusted_total_m3), fill_area: row.fill_area_m2, diff --git a/B08_Quantity/B08_Quantity_Router_Earthwork.py b/B08_Quantity/B08_Quantity_Router_Earthwork.py index 9fe57b191..8586c0b93 100644 --- a/B08_Quantity/B08_Quantity_Router_Earthwork.py +++ b/B08_Quantity/B08_Quantity_Router_Earthwork.py @@ -15,6 +15,8 @@ from __future__ import annotations import asyncio +import json +from pathlib import Path from typing import Any from uuid import UUID @@ -45,6 +47,25 @@ from config.config_db import get_db_pool router = APIRouter(prefix="/api/projects", tags=["B08 Earthwork"]) +def _mc_chainages(root: Path, section: dict[str, Any] | None) -> set[float]: + """MC(곡선 가운데 · 종단 파일 `kind` curve) 누가거리 — 토적표 측점 뒤 「 (MC)」(507-3).""" + relative = (section or {}).get("longitudinal_file_path") + path = root / str(relative) if relative else None + if path is None or not path.is_file(): + return set() + try: + stations = json.loads(path.read_text(encoding="utf-8")).get("stations") or [] + except (OSError, ValueError, AttributeError): + return set() + return { + round(float(station["chainage_m"]), 3) + for station in stations + if isinstance(station, dict) + and station.get("kind") == "curve" + and isinstance(station.get("chainage_m"), (int, float)) + } + + @router.get("/{project_id}/quantity/earthwork") async def earthwork_table(project_id: UUID) -> dict[str, Any]: root = await _project_root(project_id) @@ -61,6 +82,10 @@ async def earthwork_table(project_id: UUID) -> dict[str, Any]: soft = soft_rock_pct(settings) stations = [StationArea.from_design(row["chainage_m"], row["design"]) for row in rows] table = build_table(stations, mixed_conversion_factors(settings)) + curves = await asyncio.to_thread(_mc_chainages, root, section) + for row in table["rows"]: + if round(float(row["chainage_m"]), 3) in curves: + row["mc"] = True for record in [*table["rows"], table["totals"]]: split_rock_columns(record, soft) mass_haul = ((section or {}).get("data") or {}).get("mass_haul") or {} diff --git a/resources/tester/test_507_3_mc_label.py b/resources/tester/test_507_3_mc_label.py new file mode 100644 index 000000000..da93b8ca6 --- /dev/null +++ b/resources/tester/test_507_3_mc_label.py @@ -0,0 +1,89 @@ +"""PLAN 507-3 — MC(곡선 가운데 · 측점 `kind` curve) 강조 · B07~B09 (MC). + +B05 · B06 화면 = (MC) · 한 단계 크게 · 굵게 · 경고색(`--color-warning` — 밝음 · 어두움 테마 둘 다) · +B07 CAD 종단 표 · 횡단 제목 = 09-03 표기 뒤 「 (MC)」(평면은 5 배수 측점만 글 · MC 글 없음) · +B08 토적표(B09 인쇄가 같은 표) = 서버가 MC 줄에 `mc` · +브라우저 측점 갈아끼움 이름 = 서버 꼴 「STA… (MC)」. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +from B07_DesignDetail.B07_DesignDetail_Engine_Cad import mc_suffix +from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Long import _long_table_values +from B08_Quantity.B08_Quantity_Router_Earthwork import _mc_chainages + +ROOT = Path(__file__).resolve().parents[2] + +STATIONS = [ + {"chainage_m": 580.0, "kind": "regular", "center_z": 10.0}, + {"chainage_m": 599.0, "kind": "curve", "center_z": 10.5}, + {"chainage_m": 600.0, "kind": "regular", "center_z": 11.0}, +] + + +def _src(relative: str) -> str: + return (ROOT / relative).read_text(encoding="utf-8") + + +def test_mc_suffix_only_curve() -> None: + assert mc_suffix({"kind": "curve"}) == " (MC)" + assert mc_suffix({"kind": "regular"}) == "" + assert mc_suffix({"kind": "irregular"}) == "" + assert mc_suffix(None) == "" + + +def test_b07_long_table_station_mc() -> None: + rows = _long_table_values(STATIONS, STATIONS, [(0.0, 10.0), (700.0, 12.0)], {}, 20.0) + assert [row["station"] for row in rows] == ["No.29", "No.29+19.0 (MC)", "No.30"] + + +def test_b08_mc_chainages_from_longitudinal(tmp_path: Path) -> None: + relative = "B06_Section/longitudinal/longitudinal.json" + path = tmp_path / relative + path.parent.mkdir(parents=True) + path.write_text(json.dumps({"stations": STATIONS}), encoding="utf-8") + assert _mc_chainages(tmp_path, {"longitudinal_file_path": relative}) == {599.0} + assert _mc_chainages(tmp_path, {"longitudinal_file_path": "none.json"}) == set() + assert _mc_chainages(tmp_path, None) == set() + + +def test_b07_cross_title_mc() -> None: + support = _src("B07_DesignDetail/B07_DesignDetail_Router_Support.py") + assert support.count("+ mc_suffix(source)") == 2 # 횡단 장 제목 · 횡단 한 장 제목 + + +def test_b08_b09_earthwork_station_mc() -> None: + tables = _src("B08_Quantity/B08_Quantity_Earthwork_Tables.ts") + assert '(row.mc ? " (MC)" : "")' in tables + # B09 인쇄 토적표 = B08 표 옵션 그대로 + assert "earthworkSheetOptions(table, interval)" in _src( + "B09_Estimation/draft/B09_Draft_Print_Quantity.ts" + ) + + +def test_b05_b06_screen_mc_emphasis() -> None: + css = _src("B06_Section/B06_Section_UI_Style_Cross.css") + assert "header strong.b06-station--mc {" in css # 카드 머리 strong 규칙을 이김 + for rule in ("\n.b06-chart__station-label--mc {", "\n.b06-station--mc {"): + body = css[css.index(rule) : css.index("}", css.index(rule))] + assert "var(--color-warning)" in body + assert "var(--font-weight-bold)" in body + # 고름 빨강이 MC 색을 이김 — 뒤에 섬 + assert css.index(".b06-chart__station-label--mc {") < css.index( + ".b06-chart__station-label--selected {" + ) + assert "b06-chart__station-label--mc" in _src("B06_Section/B06_Section_UI_Longitudinal.ts") + assert 'label.className = "b06-station--mc"' in _src( + "B06_Section/B06_Section_UI_Cross_Card_Chrome.ts" + ) + cross = _src("B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts") + assert "kind: section.kind ?? null" in cross + assert '${mc ? " b06-station--mc" : ""}' in cross + ticks = _src("B04_PreProcess/B04_PreProcess_UI_MapOverlays.ts") + assert 'mc ? "bold 13px' in ticks + assert 'themeColor("--color-warning"' in ticks + swap = _src("B06_Section/logic/B06_Section_Station_Swap.ts") + assert 'formatStation(station.chainageM) + (kind === "curve" ? " (MC)" : "")' in swap