Merge remote-tracking branch 'origin/dev' into sub_laptop_7
This commit is contained in:
@@ -19,6 +19,7 @@ import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
|
||||
import { fetchDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import { readPendingPipes } from "../B05_Profile/B05_Profile_Api_Pipes_Draft";
|
||||
import type { CrossNoteCad, CrossSection, CulvertSideSpec } from "./B06_Section_Api_Fetch";
|
||||
import { pipeLengthWhole } from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
|
||||
const noteKey = (chainageM: number): string => chainageM.toFixed(2);
|
||||
|
||||
@@ -109,6 +110,9 @@ export function legacyNoteEntities(note: string | undefined): Record<string, unk
|
||||
const one = (value: number | null | undefined): string =>
|
||||
typeof value === "number" && Number.isFinite(value) ? value.toFixed(1) : "";
|
||||
const metres = (value: number | null | undefined): string => (one(value) ? `${one(value)}m` : "");
|
||||
/** 관 길이 = 사사오입 정수(265-3 · B08 「관매설」과 같은 값) · 없으면 빈칸. */
|
||||
const wholeMetres = (value: number | null | undefined): string =>
|
||||
typeof value === "number" && Number.isFinite(value) ? `${pipeLengthWhole(value)}m` : "";
|
||||
const trim = (value: number): string => String(Math.round(value * 100) / 100);
|
||||
/** 돌쌓기(찰) · 찰붙임 → 찰 · (메) · 메붙임 → 메 · 그 밖 = 그대로. */
|
||||
const bondOf = (form: string | null | undefined): string =>
|
||||
@@ -183,7 +187,7 @@ export function culvertNoteEntities(
|
||||
const inside = [
|
||||
"배수관",
|
||||
`Φ${Math.round(culvert.diameter_m * 1000)}m/m`,
|
||||
`L=${metres(section.design?.pipe_length_m)}`,
|
||||
`L=${wholeMetres(section.design?.pipe_length_m)}`,
|
||||
];
|
||||
const right = [
|
||||
station,
|
||||
|
||||
@@ -360,6 +360,11 @@
|
||||
fill: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* 관 정보 글(관매설 · 세월교 · 267-1) — 노폭 라벨(`.b06-chart__carriageway-label`)과 같은 글 크기 */
|
||||
.b06-chart__culvert-label.b06-chart__pipe-info {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* 유입 보호공 공간 확보 절토선 — 말단에서 표준단면 절토경사로 지반까지 */
|
||||
.b06-chart__culvert-cut {
|
||||
stroke: var(--color-warning);
|
||||
|
||||
@@ -40,6 +40,11 @@ def _metres(value: Any) -> str:
|
||||
return f"{_one(value)}m" if _one(value) else ""
|
||||
|
||||
|
||||
def _whole_metres(value: Any) -> str:
|
||||
"""관 길이 = 사사오입 정수(265-3 · TS `pipeLengthWhole` · B08 「관매설」과 같은 값)."""
|
||||
return f"{_js_round(value)}m" if _one(value) else ""
|
||||
|
||||
|
||||
def _trim(value: float) -> str:
|
||||
rounded = _js_round(value * 100) / 100
|
||||
return str(int(rounded)) if rounded == int(rounded) else str(rounded)
|
||||
@@ -159,7 +164,7 @@ def culvert_note_entities(
|
||||
inside = [
|
||||
"배수관",
|
||||
f"Φ{_js_round(float(culvert.get('diameter_m') or 0.0) * 1000)}m/m",
|
||||
f"L={_metres(length)}",
|
||||
f"L={_whole_metres(length)}",
|
||||
]
|
||||
chute = culvert.get("chute") if isinstance(culvert.get("chute"), dict) else {}
|
||||
right = [
|
||||
|
||||
@@ -204,6 +204,19 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 262-2 고정 시점 표지 — 둥근 붉은 알약(고르는 표지 = 기본 핀 + 흰 이름표와 구별) */
|
||||
.b08-milestone__map-start {
|
||||
padding: 3px 10px;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 999px;
|
||||
background: #e60046;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 등록 · 연결선 모달(244-13) — 더 큰 창 · 2열 칸 */
|
||||
.ui-modal.b08-milestone__modal {
|
||||
width: min(960px, 94vw);
|
||||
|
||||
@@ -98,6 +98,8 @@ export interface MapPin {
|
||||
lon: number;
|
||||
lat: number;
|
||||
movable: boolean;
|
||||
/** 262-2 고정 시점 표지 — 끌기 · 맞춤 · 보기 이동에 안 쓰임 · 고르는 표지와 모양이 다름 */
|
||||
fixed?: boolean;
|
||||
}
|
||||
|
||||
export interface EditMap {
|
||||
@@ -127,8 +129,9 @@ export function editMap(
|
||||
loadKakaoMaps(projectId).then(
|
||||
(maps) =>
|
||||
requestAnimationFrame(() => {
|
||||
const moving = pins.filter((p) => !p.fixed);
|
||||
const points = [
|
||||
...pins.map((p) => new maps.LatLng(p.lat, p.lon)),
|
||||
...moving.map((p) => new maps.LatLng(p.lat, p.lon)),
|
||||
...lines.flat().map(([lat, lon]) => new maps.LatLng(lat, lon)),
|
||||
];
|
||||
const c = opts.center?.();
|
||||
@@ -156,6 +159,17 @@ export function editMap(
|
||||
),
|
||||
...pins.flatMap((p) => {
|
||||
const at = new maps.LatLng(p.lat, p.lon);
|
||||
if (p.fixed)
|
||||
return [
|
||||
new maps.CustomOverlay({
|
||||
position: at,
|
||||
content: el("span", {
|
||||
className: "b08-milestone__map-start",
|
||||
text: p.name,
|
||||
}),
|
||||
yAnchor: 0.5,
|
||||
}),
|
||||
];
|
||||
const marker = new maps.Marker({ position: at, title: p.name, draggable: p.movable });
|
||||
if (p.movable)
|
||||
maps.event.addListener(marker, "dragend", () => {
|
||||
@@ -176,7 +190,7 @@ export function editMap(
|
||||
if (fitted) {
|
||||
// 보기 밖 표지만 그쪽으로 옮김(줌 그대로)
|
||||
const view = map.getBounds();
|
||||
const out = pins.find((p) => !view.contain(new maps.LatLng(p.lat, p.lon)));
|
||||
const out = moving.find((p) => !view.contain(new maps.LatLng(p.lat, p.lon)));
|
||||
if (out) map.panTo(new maps.LatLng(out.lat, out.lon));
|
||||
} else if (points.length > 1) {
|
||||
const bounds = new maps.LatLngBounds();
|
||||
|
||||
@@ -106,8 +106,8 @@ export function openPlaceModal(
|
||||
const title = target.add
|
||||
? "요소 추가 — 기준점 · 거래처"
|
||||
: base
|
||||
? `기준점 — ${base.name || "기준점"}`
|
||||
: `거래처 — ${route!.name || "거래처"}`; // 254-3 표 머리 글(항목 정보) 없음
|
||||
? base.name || "기준점"
|
||||
: route!.name || "거래처"; // 262-1 「—」 앞 글자 없음
|
||||
openModal({
|
||||
title,
|
||||
closeLabel: "닫기",
|
||||
@@ -259,7 +259,7 @@ export interface SiteAddress {
|
||||
/** 시점(공사지) 상자 — 중점 측점(km · 저장은 m) · 시점 → 중점 도로 · 아래 카카오 지도(시점 · 중점 표지) */
|
||||
export function openSiteInfo(projectId: string): void {
|
||||
openModal({
|
||||
title: "공사지 — 시점 · 중점",
|
||||
title: "시점 · 중점",
|
||||
closeLabel: "닫기",
|
||||
dialogClass: "b08-milestone__modal",
|
||||
mount: (body, close, actions) => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { createButton, createInputField, el, showToast } from "@ui/ui_template_e
|
||||
import { searchAddress, type AddressCandidate } from "../B01_Dashboard/B01_Dashboard_Api_Fetch";
|
||||
import { request } from "./B08_Quantity_UI_Hydro";
|
||||
import { editMap } from "./B08_Quantity_UI_Milestone_KakaoMap";
|
||||
import { siteInfo } from "./B08_Quantity_UI_Milestone_State";
|
||||
import { liveMilestone, startPoint } from "./B08_Quantity_UI_Milestone_State";
|
||||
|
||||
export interface Place {
|
||||
address: string;
|
||||
@@ -52,27 +52,35 @@ export function placePicker(projectId: string, initial: Place, name: () => strin
|
||||
list.hidden = true;
|
||||
const where = el("p", { className: "b08-milestone__place" });
|
||||
where.dataset.b08MilestonePlace = "value";
|
||||
// 262-2 시점 = 거리이정표가 쓰는 시점(끌어 옮긴 덮개 · 없으면 노선 시점)
|
||||
const start = (): [number, number] | null => startPoint(liveMilestone(projectId));
|
||||
const map = editMap(projectId, {
|
||||
hint: "지도 누름 = 그 자리를 소재지로 · 표지 끌어 옮김",
|
||||
onClick: (lon, lat) => void moveTo(lon, lat),
|
||||
onMove: (_key, lon, lat) => void moveTo(lon, lat),
|
||||
center: () => siteInfo()?.start_lonlat ?? null,
|
||||
center: start,
|
||||
});
|
||||
|
||||
const draw = (): void => {
|
||||
where.textContent = `소재지 — ${place.address || "주소 찾기 · 지도에서 고르세요"}`;
|
||||
const s = start();
|
||||
map.set(
|
||||
place.lon != null && place.lat != null
|
||||
? [
|
||||
{
|
||||
key: "place",
|
||||
name: name() || "소재지",
|
||||
lon: place.lon,
|
||||
lat: place.lat,
|
||||
movable: true,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
[
|
||||
...(s
|
||||
? [{ key: "start", name: "시점", lon: s[0], lat: s[1], movable: false, fixed: true }]
|
||||
: []),
|
||||
...(place.lon != null && place.lat != null
|
||||
? [
|
||||
{
|
||||
key: "place",
|
||||
name: name() || "소재지",
|
||||
lon: place.lon,
|
||||
lat: place.lat,
|
||||
movable: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
[],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -28,6 +28,10 @@ function loadTs(file) {
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
// 관 길이 정수(265-3) = 실제 `pipeLengthWhole`(타입 import 만 있는 파일)
|
||||
STUBS["./B06_Section_UI_Cross_Culvert_Const"] = loadTs(
|
||||
path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Culvert_Const.ts"),
|
||||
);
|
||||
const cad = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Detail_Cad.ts"));
|
||||
|
||||
const wall = (role, form, height, length) => ({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""254-3 B08 거리이정표 상자 모달 제목 · 장소 그림(사용자 162-6 · 8).
|
||||
|
||||
① 상자 누름 모달 제목 = 「거래처 — 이름」 · 「기준점 — 이름」(표 머리 글 「항목 정보」 없음)
|
||||
① 상자 누름 모달 제목 = 이름만(262-1 · 「—」 앞 글자 없음 · 표 머리 글 「항목 정보」 없음)
|
||||
② 장소 그림(256-1 `place_images`) = 상자 안 글 아래 작게 · 그림 없으면 자리 없음(상자 높이 그대로)
|
||||
"""
|
||||
|
||||
@@ -67,7 +67,7 @@ def test_장소_그림_상자_안_글_아래():
|
||||
|
||||
def test_모달_제목_화면_그림():
|
||||
modal = _read(B08 + "UI_Milestone_Modal.ts")
|
||||
assert "`거래처 — ${route!.name" in modal and "`기준점 — ${base.name" in modal
|
||||
assert 'route!.name || "거래처"' in modal and 'base.name || "기준점"' in modal # 262-1
|
||||
assert "항목 정보 —" not in modal
|
||||
ui = _read(B08 + "UI_Milestone_Diagram.ts")
|
||||
assert "placeImageBlobUrl(projectId, id)" in ui and "placeImageKey(id)" in ui
|
||||
|
||||
@@ -107,7 +107,7 @@ def test_관_유입_유출_도수로_한_벌(out):
|
||||
assert _labels(full) == [
|
||||
"배수관",
|
||||
"Φ1200m/m",
|
||||
"L=22.0m",
|
||||
"L=22m",
|
||||
"NO.71+13",
|
||||
"관보호공(유입) 기슭막이(찰,H=2.0) L=10.0m",
|
||||
"관보호공(유출) 기슭막이(메,H=2.0) L=10.0m",
|
||||
@@ -136,7 +136,7 @@ def test_도수로_옵션이_없으면_세트_구조만(out):
|
||||
|
||||
|
||||
def test_보호공을_안_고르면_관과_측점만(out):
|
||||
assert _labels(out["noSides"]) == ["배수관", "Φ1200m/m", "L=22.0m", "NO.71+13"]
|
||||
assert _labels(out["noSides"]) == ["배수관", "Φ1200m/m", "L=22m", "NO.71+13"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("case", ["none", "ford", "hidden", "neighbour"])
|
||||
@@ -425,7 +425,7 @@ def test_칸_없음_만_미리_넣음_지운_칸_옛_글_설계없음_이웃은_
|
||||
assert labels[2:] == [
|
||||
"배수관",
|
||||
"Φ1200m/m",
|
||||
"L=22.0m",
|
||||
"L=22m",
|
||||
"NO.71+13",
|
||||
"관보호공(유입) 기슭막이(찰,H=2.0) L=10.0m",
|
||||
"관보호공(유출) 기슭막이(메,H=2.0) L=10.0m",
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"""262 B08 거리이정표 상자 창 제목 · 소재지 지도 시점 표지(사용자 164-1).
|
||||
|
||||
262-1 거래처 · 기준점 창 제목 = 이름만(빈 이름 = 「거래처」 · 「기준점」) · 시점 창 = 「시점 · 중점」 · 요소 추가 창 그대로
|
||||
262-2 소재지 지도 = 시점 표지 고정(끌기 없음 · 맞춤에서 빠짐) · 시점 없으면 표지 없음 · 소재지 없으면 시점 중심
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
B08 = "B08_Quantity/B08_Quantity_UI_Milestone"
|
||||
|
||||
|
||||
def _read(name):
|
||||
return (ROOT / f"{B08}{name}").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_제목_이름만():
|
||||
modal = _read("_Modal.ts")
|
||||
title = modal[modal.index("const title = target.add") : modal.index("openModal({")]
|
||||
assert '? base.name || "기준점"' in title and ': route!.name || "거래처"' in title
|
||||
assert '"요소 추가 — 기준점 · 거래처"' in title
|
||||
assert 'title: "시점 · 중점",' in modal and "공사지 —" not in modal
|
||||
assert re.search(r"title: `?[^\n]*(거래처|기준점|공사지) —", modal) is None
|
||||
|
||||
|
||||
def test_시점_표지_고정():
|
||||
place = _read("_Place.ts")
|
||||
assert "startPoint(liveMilestone(projectId))" in place # 거리이정표가 쓰는 시점
|
||||
assert "center: start" in place # 소재지 없음 = 시점 중심
|
||||
assert "movable: false, fixed: true" in place and 'name: "시점"' in place
|
||||
assert "...(s" in place and " : [])," in place # 시점 없음 = 표지 없음
|
||||
kakao = _read("_KakaoMap.ts")
|
||||
assert "fixed?: boolean;" in kakao
|
||||
assert "const moving = pins.filter((p) => !p.fixed);" in kakao
|
||||
assert "...moving.map(" in kakao # 처음 보기 = 소재지 중심(시점은 맞춤에서 빠짐)
|
||||
assert "moving.find(" in kakao # 보기 이동에서도 빠짐
|
||||
assert "if (p.fixed)" in kakao and "b08-milestone__map-start" in kakao
|
||||
css = _read(".css")
|
||||
assert ".b08-milestone__map-start {" in css
|
||||
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""PLAN 265 — B06 [상세] 표 위 CAD 칸 · 접기 · 차례 · 구조물 글 · 관 길이 정수.
|
||||
|
||||
- 265-3 원형 마크 L = 사사오입 정수(TS `pipeLengthWhole` = 파이썬 짝 같은 값).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_265_note_cad.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from B06_Section.B06_Section_Engine_Culvert import _culvert_set # noqa: E402
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_CulvertNote import ( # noqa: E402
|
||||
culvert_note_entities,
|
||||
)
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
_PIPE = {"pipe_diameter_mm": 800}
|
||||
|
||||
|
||||
def _mirror(cases: list[dict]) -> list[dict]:
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_260_note_cad.cjs"), "mirror"],
|
||||
input=json.dumps(cases, ensure_ascii=False),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def _labels(entities: list[dict]) -> list[str]:
|
||||
return [e["shapeData"]["label"] for e in entities if e["type"] == "Text"]
|
||||
|
||||
|
||||
# ── 265-3 관 길이 정수 ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("length", "label"),
|
||||
[(8.5, "L=9m"), (8.4, "L=8m"), (8.45, "L=8m"), (7.0, "L=7m"), (None, "L=")],
|
||||
)
|
||||
def test_원형_마크_L_은_사사오입_정수_TS_파이썬_같음(length, label):
|
||||
design = {} if length is None else {"pipe_length_m": length}
|
||||
culvert = {**_culvert_set(_PIPE), "chainage_m": 40.0}
|
||||
case = {
|
||||
"section": {"chainage_m": 40.0, "culvert": culvert, "design": design},
|
||||
"station": "NO.2",
|
||||
"options": _PIPE,
|
||||
"chainage": 40.0,
|
||||
"interval": 20.0,
|
||||
}
|
||||
[ts] = _mirror([case])
|
||||
py = culvert_note_entities(40.0, culvert, design, "NO.2", _PIPE)
|
||||
assert _labels(ts["entities"])[2] == label
|
||||
assert _labels(py)[2] == label
|
||||
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""PLAN 267-1 — B06 횡단 카드 「관매설 …」 글 크기 = 「노폭 …」 글 크기 · 줄 겹침 없음.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_267_1_pipe_info_font.py -q -p no:cacheprovider --basetemp=tmp/pt
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
CSS = (ROOT / "B06_Section" / "B06_Section_UI_Style_Cross_Areas.css").read_text(encoding="utf-8")
|
||||
TYPES = (ROOT / "B06_Section" / "B06_Section_UI_Cross_Culvert_Types.ts").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _font_px(selector: str) -> float:
|
||||
"""셀렉터 블록의 마지막 font-size(px) — 뒤에 오는 규칙이 이긴다."""
|
||||
blocks = re.findall(re.escape(selector) + r"\s*\{([^}]*)\}", CSS)
|
||||
sizes = [float(m) for b in blocks for m in re.findall(r"font-size:\s*([\d.]+)px", b)]
|
||||
assert sizes, selector
|
||||
return sizes[-1]
|
||||
|
||||
|
||||
def test_관_정보_글_크기가_노폭_라벨과_같다() -> None:
|
||||
road = _font_px(".b06-chart__carriageway-label")
|
||||
info = _font_px(".b06-chart__culvert-label.b06-chart__pipe-info")
|
||||
assert road == info == 11.0
|
||||
# 다른 culvert-label(집수정 · 박스 이름)은 그대로 9px
|
||||
assert _font_px(".b06-chart__culvert-label") == 9.0
|
||||
|
||||
|
||||
def test_관_정보_글_줄이_노폭_라벨과_겹치지_않는다() -> None:
|
||||
step = float(re.search(r"- (\d+) \* \(stacked \+ 1\)", TYPES).group(1))
|
||||
font = _font_px(".b06-chart__culvert-label.b06-chart__pipe-info")
|
||||
road_font = _font_px(".b06-chart__carriageway-label")
|
||||
asc, desc = 0.8, 0.2 # 글자 상자 근사 — 기준선 위 80% · 아래 20%
|
||||
road_top = 0 - asc * road_font # 노폭 라벨 기준선 y=0
|
||||
for n in (1, 2, 3): # 관 정보 글이 쌓인 줄 수
|
||||
info_y = -step * n
|
||||
info_bottom = info_y + desc * font
|
||||
assert info_bottom < road_top, (n, info_bottom, road_top)
|
||||
if n > 1: # 윗줄 바닥 < 아랫줄 윗면
|
||||
upper_bottom = -step * n + desc * font
|
||||
lower_top = -step * (n - 1) - asc * font
|
||||
assert upper_bottom < lower_top, n
|
||||
Reference in New Issue
Block a user