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/docs/raw/요구_1021/사용자요구_1021.md b/docs/raw/요구_1021/사용자요구_1021.md index 600f75e0b..6581e8648 100644 --- a/docs/raw/요구_1021/사용자요구_1021.md +++ b/docs/raw/요구_1021/사용자요구_1021.md @@ -21,3 +21,12 @@ 4. b05에서 mc는 사용자가 수정해야할 부분임. 이때 mc도 붙이고 글자크기 키우고 볼트체와 색상을 변경. b07~b09에도 남길것. 5. 검증 스크립트는 프로그램 개발에 도움을 주기 위한 스크립트는 너의 재량것 사용. 6. 서브3 백업사본은 삭제 + +## 요구 312 (10-21 · 사용자 · 계산 자리 창) + +1. 계산위치 제어 페이지사 제어안됨? 추가로로직은 45개 아님? +2. (물음 답) 45줄 전체 표시 + +## 요구 313 (10-21 · 사용자 · 311-2 고침) + +1. 노출형 횡단수로와 개거에 대해 잘 못 지시함. 다른 구조물 중 취수정이나 국가지점 번호 처럼 동작해야함. 되돌리면 될것같음. 확인 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