From ecf99aefde7357314cf0491fccf98f3dcbdf6eba Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 10 Oct 2026 21:39:16 +0900 Subject: [PATCH] =?UTF-8?q?feat(b08):=20=EA=B1=B0=EB=A6=AC=EC=9D=B4?= =?UTF-8?q?=EC=A0=95=ED=91=9C=20=EA=B8=B0=EC=A4=80=EC=A0=90=20=EB=B0=A9?= =?UTF-8?q?=EC=9C=84=20=EC=B9=B8=20=EC=97=86=EC=95=B0=20=E2=80=94=20?= =?UTF-8?q?=EC=A3=BC=EC=86=8C=20=EC=A2=8C=ED=91=9C=EB=A1=9C=20=EC=9E=90?= =?UTF-8?q?=EB=A6=AC=20=C2=B7=20=EC=97=86=EC=9C=BC=EB=A9=B4=20=EA=B3=B5?= =?UTF-8?q?=EC=82=AC=EC=A7=80=20=EC=84=9C=EC=AA=BD=20(244-5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 좌측 패널 「공사지에서 본 방위」 칸 · 8방 고르기 지움 · 옛 저장 값 방위는 무시 - 기준점 모달에 「주소를 고르면 실제 방위로 놓임」 안내 한 줄 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz --- B08_Quantity/B08_Quantity_Milestone_Layout.ts | 4 +- .../B08_Quantity_UI_Milestone_Modal.ts | 4 ++ .../B08_Quantity_UI_Milestone_State.ts | 31 +-------- .../test_244_5_b08_milestone_base_dir.py | 63 +++++++++++++++++++ 4 files changed, 72 insertions(+), 30 deletions(-) create mode 100644 resources/tester/test_244_5_b08_milestone_base_dir.py diff --git a/B08_Quantity/B08_Quantity_Milestone_Layout.ts b/B08_Quantity/B08_Quantity_Milestone_Layout.ts index accdb206d..77b558157 100644 --- a/B08_Quantity/B08_Quantity_Milestone_Layout.ts +++ b/B08_Quantity/B08_Quantity_Milestone_Layout.ts @@ -6,7 +6,7 @@ * 업체 자리 = 기준점(거침 · 없으면 공사지) 기준 지도 방위 — 업체 경위도(모달 주소 검색)가 있으면 * 방위각 자동(244-4 · 기준점 좌표 없으면 시점 좌표로) · 없으면 옛 8방 · 그도 없으면 빈 방위 차례 · * 같은 방위 여럿 = 옆으로 나란히 - * 기준점 자리 = 공사지 기준 같은 규칙(경위도 · 8방 · 기본 서) + * 기준점 자리 = 기준점 주소 좌표 → 공사지(시점) 기준 방위각 · 좌표 없으면 서(왼 · 견본) — 옛 8방 무시(244-5) * 선 = 업체 → 기준점(또는 시점) · 기준점 → 시점 · 시점 → 중점 — 글 = 「도로 구분 km」 줄마다 * 화면 = 북 위(SVG y 아래로 = 남) * 선 글 바로 고침(244-4 ⑥) = `legAt` · `setLeg` — 선(from) · 글 차례 → 그 구간(모달과 같은 값 한 벌) @@ -144,7 +144,7 @@ export function layoutMilestone( data.base.lon != null && data.base.lat != null ? [data.base.lon, data.base.lat] : null; let baseDeg = 270; if (useBase) { - baseDeg = placeDeg(data.base, startLL) ?? 270; + baseDeg = placeDeg({ dir: null, lon: data.base.lon, lat: data.base.lat }, startLL) ?? 270; const [ux, uy] = unit(baseDeg); const cx = ux * R_BASE; const cy = uy * R_BASE; diff --git a/B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts b/B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts index c3b39e9ea..c18484595 100644 --- a/B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts +++ b/B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts @@ -292,6 +292,10 @@ export function openBaseInfo(projectId: string): void { head("공통 구간 — 기준점 → 시점 (km · 도로 구분)"), legs.root, head("주소 · 지도"), + el("p", { + className: "b08-options__hint", + text: "주소를 고르면 다이어그램에 실제 방위로 놓임 — 안 고르면 공사지 서쪽(왼쪽) 기본 자리", + }), address.root, ); }, diff --git a/B08_Quantity/B08_Quantity_UI_Milestone_State.ts b/B08_Quantity/B08_Quantity_UI_Milestone_State.ts index 6a53b8625..45198f8fd 100644 --- a/B08_Quantity/B08_Quantity_UI_Milestone_State.ts +++ b/B08_Quantity/B08_Quantity_UI_Milestone_State.ts @@ -12,7 +12,7 @@ import { createCheckbox } from "@ui/ui_template_checkbox"; import { createInputField, createSelectField, el } from "@ui/ui_template_elements"; import { clearState, readState, writeState } from "../A00_Common/b_page_state"; import { request } from "./B08_Quantity_UI_Hydro"; -import { DIRS, ROADS, type MilestoneData, type Road } from "./B08_Quantity_Milestone_Calc"; +import { ROADS, type MilestoneData, type Road } from "./B08_Quantity_Milestone_Calc"; export interface SiteInfo { length_m: number | null; @@ -27,18 +27,6 @@ interface Payload { images: Record; } -/** 8방 이름 */ -export const DIR_TEXT: Record = { - N: "북", - NE: "북동", - E: "동", - SE: "남동", - S: "남", - SW: "남서", - W: "서", - NW: "북서", -}; - export const EMPTY: MilestoneData = { site: { mid_station_m: null, mid_road: "비포장" }, base: { enabled: false, name: "", address: "", dir: null, legs: [] }, @@ -139,15 +127,11 @@ export async function imageBlobUrl(projectId: string, routeId: string): Promise< } export const roadOptions = ROADS.map((r) => ({ value: r, text: r })); -export const dirOptions = [ - { value: "", text: "방위 —" }, - ...DIRS.map((d) => ({ value: d, text: DIR_TEXT[d] })), -]; const km = (m: number | null | undefined): string => m == null ? "-" : (m / 1000).toLocaleString(undefined, { maximumFractionDigits: 3 }); -/** 좌측 패널 「거리이정표」 카드 몸 — 공사지(시점 · 중점) · 기준점(있음 · 이름 · 주소 · 방위) */ +/** 좌측 패널 「거리이정표」 카드 몸 — 공사지(시점 · 중점) · 기준점(있음 · 이름 · 주소 · 방위 = 주소 좌표 자동) */ export function milestoneCardBody(projectId: string | null): HTMLElement[] { const mid = createInputField({ label: "중점 측점 (m)", type: "number", compact: true, min: 0 }); const midRoad = createSelectField({ @@ -159,14 +143,9 @@ export function milestoneCardBody(projectId: string | null): HTMLElement[] { const has = createCheckbox("기준점 있음"); const name = createInputField({ label: "기준점 이름", compact: true }); const address = createInputField({ label: "기준점 주소", compact: true }); - const dir = createSelectField({ - label: "공사지에서 본 방위", - options: dirOptions, - compact: true, - }); mid.input.dataset.b08Milestone = "mid_station_m"; has.input.dataset.b08Milestone = "base_enabled"; - const baseFields = el("div", { children: [name.root, address.root, dir.root] }); + const baseFields = el("div", { children: [name.root, address.root] }); const show = (): void => { if (!projectId) return; @@ -183,7 +162,6 @@ export function milestoneCardBody(projectId: string | null): HTMLElement[] { baseFields.hidden = !data.base.enabled; if (document.activeElement !== name.input) name.input.value = data.base.name; if (document.activeElement !== address.input) address.input.value = data.base.address; - dir.select.value = data.base.dir ?? ""; }; const edit = (fn: (data: MilestoneData) => void): void => { if (!projectId) return; @@ -210,9 +188,6 @@ export function milestoneCardBody(projectId: string | null): HTMLElement[] { address.input.addEventListener("input", () => edit((d) => (d.base.address = address.input.value)), ); - dir.select.addEventListener("change", () => - edit((d) => (d.base.dir = (dir.select.value || null) as MilestoneData["base"]["dir"])), - ); const host = el("div", { children: [mid.root, midRoad.root, siteHint, has.root, baseFields], diff --git a/resources/tester/test_244_5_b08_milestone_base_dir.py b/resources/tester/test_244_5_b08_milestone_base_dir.py new file mode 100644 index 000000000..716657f71 --- /dev/null +++ b/resources/tester/test_244_5_b08_milestone_base_dir.py @@ -0,0 +1,63 @@ +"""244-5 B08 거리이정표 기준점 방위 칸 없앰 — 기준점 자리 = 주소 좌표 → 공사지 기준 방위각 · +좌표 없으면 공사지 서쪽(견본) · 옛 저장 값의 방위(dir)는 무시. +""" + +import json +import re +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE) + + +def _read(rel): + return (ROOT / rel).read_text(encoding="utf-8") + + +def _layout(base, site): + data = {"site": {"mid_station_m": None, "mid_road": "비포장"}, "base": base, "routes": []} + src = IMPORTS.sub("", _read("B08_Quantity/B08_Quantity_Milestone_Layout.ts")) + expr = f"layoutMilestone({json.dumps(data)}, {json.dumps(site)}, 0)" + result = subprocess.run( + ["node", "--experimental-strip-types", "--no-warnings", + "--input-type=module-typescript", "-e", src + f"\nconsole.log(JSON.stringify({expr}));"], + capture_output=True, text=True, encoding="utf-8", timeout=60, + ) # fmt: skip + assert result.returncode == 0, result.stderr + lay = json.loads(result.stdout) + center = {b["id"]: (b["x"] + b["w"] / 2, b["y"] + b["h"] / 2) for b in lay["boxes"]} + return center["site"], center["base"] + + +BASE = {"enabled": True, "name": "기준", "address": "", "legs": []} +SITE = {"start_lonlat": [127.8, 35.7], "mid_lonlat": None} + + +def test_좌표_없음_서쪽_옛_방위_무시(): + for site in (SITE, None): + (cx, cy), (bx, by) = _layout({**BASE, "dir": "E"}, site) + assert bx < cx - 200 and abs(by - cy) < 1 # 옛 「동」 무시 · 서(왼) + + +def test_좌표_있음_공사지_기준_방위각(): + (cx, cy), (bx, by) = _layout({**BASE, "dir": "W", "lon": 127.8, "lat": 35.8}, SITE) + assert abs(bx - cx) < 1 and by < cy - 200 # 북 = 위(옛 「서」 무시) + (cx, cy), (bx, by) = _layout({**BASE, "dir": None, "lon": 127.9, "lat": 35.7}, SITE) + assert bx > cx + 200 and abs(by - cy) < 1 # 동 = 오른 + # 시점 좌표 모름 = 서쪽 기본 + (cx, cy), (bx, by) = _layout({**BASE, "lon": 127.9, "lat": 35.7}, None) + assert bx < cx - 200 + + +def test_좌측_패널_방위_칸_없음_모달_안내(): + state = _read("B08_Quantity/B08_Quantity_UI_Milestone_State.ts") + assert "공사지에서 본 방위" not in state and "dirOptions" not in state + assert "d.base.dir" not in state and 'label: "기준점 주소"' in state + modal = _read("B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts") + base = modal[modal.index("export function openBaseInfo") : modal.index("interface SiteAddress")] + assert "주소를 고르면 다이어그램에 실제 방위로 놓임" in base + layout = _read("B08_Quantity/B08_Quantity_Milestone_Layout.ts") + assert ( + "placeDeg({ dir: null, lon: data.base.lon, lat: data.base.lat }, startLL) ?? 270" in layout + )