feat(b08): 거리이정표 다이어그램 선 글 더블클릭 = km · 도로 구분 바로 고침 (244-4 ⑥)
- 선 글 한 줄 = 그 구간(업체 · 공통 · 시점 → 중점) · 글 화면 자리에 도로 구분 ▾ · km 칸 · Enter · 칸 밖 = 반영 · Esc = 취소 - 값은 모달과 한 벌(legAt · setLeg) · 시점 → 중점 km = 중점 측점 · 표 셈 그대로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
* 기준점 자리 = 공사지 기준 같은 규칙(경위도 · 8방 · 기본 서)
|
||||
* 선 = 업체 → 기준점(또는 시점) · 기준점 → 시점 · 시점 → 중점 — 글 = 「도로 구분 km」 줄마다
|
||||
* 화면 = 북 위(SVG y 아래로 = 남)
|
||||
* 선 글 바로 고침(244-4 ⑥) = `legAt` · `setLeg` — 선(from) · 글 차례 → 그 구간(모달과 같은 값 한 벌)
|
||||
* ========================================================================== */
|
||||
|
||||
import type { Dir, Leg, MilestoneData } from "./B08_Quantity_Milestone_Calc";
|
||||
@@ -227,3 +228,29 @@ export function kakaoRouteUrl(points: Array<{ name: string; lon: number; lat: nu
|
||||
`${encodeURIComponent(p.name.replace(/[,/]/g, " ").trim() || "지점")},${p.lat},${p.lon}`;
|
||||
return `https://map.kakao.com/link/by/car/${points.map(part).join("/")}`;
|
||||
}
|
||||
|
||||
/** 선 글 한 줄 → 그 구간 — from = 업체 id(업체 구간) · "base"(공통 구간) · "start"(시점 → 중점) */
|
||||
export function legAt(data: MilestoneData, from: string, i: number, midKm: number): Leg | null {
|
||||
if (from === "start") return { road: data.site.mid_road, km: Math.round(midKm * 1000) / 1000 };
|
||||
const legs = from === "base" ? data.base.legs : data.routes.find((r) => r.id === from)?.legs;
|
||||
return legs?.[i] ?? null;
|
||||
}
|
||||
|
||||
/** 선 글에서 고친 값 → 데이터(시점 → 중점 km = 중점 측점 m) · 고친 것만 */
|
||||
export function setLeg(
|
||||
data: MilestoneData,
|
||||
from: string,
|
||||
i: number,
|
||||
patch: { road?: Leg["road"]; km?: number | null },
|
||||
): MilestoneData {
|
||||
if (from === "start") {
|
||||
if (patch.road) data.site.mid_road = patch.road;
|
||||
if (patch.km !== undefined)
|
||||
data.site.mid_station_m = patch.km == null ? null : Math.round(patch.km * 1000 * 1000) / 1000;
|
||||
return data;
|
||||
}
|
||||
const legs = from === "base" ? data.base.legs : data.routes.find((r) => r.id === from)?.legs;
|
||||
const leg = legs?.[i];
|
||||
if (leg) Object.assign(leg, patch);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -125,3 +125,25 @@
|
||||
.b08-milestone__diagram svg.is-panning {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* 선 글 바로 고침(244-4 ⑥) — 글 아래 작은 판 */
|
||||
.b08-milestone__km tspan {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.b08-milestone__leg-edit {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 6px;
|
||||
padding: 6px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background: var(--color-surface);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.b08-milestone__leg-edit .ui-input {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
@@ -5,20 +5,26 @@
|
||||
* 상자 = 공사지(시점 · 중점 한 컨테이너) · 기준점 · 업체들 · 선마다 「도로 구분 km」 글
|
||||
* 상자 누름 = 모달(244-4 · 탭 이동 아님 · `…_UI_Milestone_Modal`) — 업체 = 항목 정보 · 공사지 = 시점 주소 ·
|
||||
* 기준점 = 공통 구간 · 공사지 · 기준점 상자 [+] = 항목 추가 모달 · 조작 = 캐시(`…_UI_Milestone_State`)
|
||||
* 팬 · 줌 = 공용 `A00_Common/b_svg_zoom_pan`(휠 = 줌 · 가운데 버튼 끌기 = 팬 · 더블클릭 = 원배율)
|
||||
* 팬 · 줌 = 공용 `A00_Common/b_svg_zoom_pan`(휠 = 줌 · 가운데 버튼 끌기 = 팬 · 빈 곳 더블클릭 = 원배율)
|
||||
* 선 글 더블클릭 = 그 구간 도로 구분 ▾ · km 바로 고침(⑥ · 글 화면 자리에 띄움 · Enter = 반영 · Esc = 취소)
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { createSelectField, el } from "@ui/ui_template_elements";
|
||||
import { attachSvgZoomPan, type ZoomPanState } from "../A00_Common/b_svg_zoom_pan";
|
||||
import { midKm, type MilestoneRoute } from "./B08_Quantity_Milestone_Calc";
|
||||
import { layoutMilestone, type DiagramBox } from "./B08_Quantity_Milestone_Layout";
|
||||
import { layoutMilestone, legAt, setLeg, type DiagramBox } from "./B08_Quantity_Milestone_Layout";
|
||||
import {
|
||||
openAddItem,
|
||||
openBaseInfo,
|
||||
openRouteInfo,
|
||||
openSiteInfo,
|
||||
} from "./B08_Quantity_UI_Milestone_Modal";
|
||||
import { liveMilestone, siteInfo } from "./B08_Quantity_UI_Milestone_State";
|
||||
import {
|
||||
liveMilestone,
|
||||
putMilestone,
|
||||
roadOptions,
|
||||
siteInfo,
|
||||
} from "./B08_Quantity_UI_Milestone_State";
|
||||
|
||||
const NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
@@ -33,6 +39,56 @@ function svg<K extends keyof SVGElementTagNameMap>(
|
||||
return node;
|
||||
}
|
||||
|
||||
/** 선 글 바로 고침 — 글 화면 자리에 도로 구분 ▾ · km 칸 · Enter · 칸 밖 = 반영 · Esc = 취소 */
|
||||
function editLegAt(
|
||||
projectId: string,
|
||||
anchor: Element,
|
||||
from: string,
|
||||
i: number,
|
||||
midKm: number,
|
||||
): void {
|
||||
const leg = legAt(liveMilestone(projectId), from, i, midKm);
|
||||
if (!leg) return;
|
||||
document.querySelector(".b08-milestone__leg-edit")?.remove();
|
||||
const road = createSelectField({ label: "도로 구분", options: roadOptions, compact: true });
|
||||
road.select.value = leg.road;
|
||||
const km = el("input", {
|
||||
className: "ui-input",
|
||||
attrs: { type: "number", min: "0", step: "any" },
|
||||
});
|
||||
km.value = leg.km == null ? "" : String(leg.km);
|
||||
km.setAttribute("aria-label", "km");
|
||||
const box = el("div", { className: "b08-milestone__leg-edit", children: [road.root, km] });
|
||||
box.dataset.b08MilestoneLegEdit = `${from}:${i}`;
|
||||
const at = anchor.getBoundingClientRect(); // 팬 · 줌 뒤 글 자리 그대로
|
||||
box.style.left = `${at.left}px`;
|
||||
box.style.top = `${at.bottom + 4}px`;
|
||||
let done = false;
|
||||
const finish = (apply: boolean): void => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
box.remove();
|
||||
if (!apply) return;
|
||||
const n = Number(km.value);
|
||||
const value = km.value.trim() && Number.isFinite(n) && n >= 0 ? n : null;
|
||||
const patch = {
|
||||
road: road.select.value as typeof leg.road,
|
||||
...(km.value.trim() && value === null ? {} : { km: value }),
|
||||
};
|
||||
putMilestone(projectId, setLeg(liveMilestone(projectId), from, i, patch));
|
||||
};
|
||||
box.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") finish(true);
|
||||
if (e.key === "Escape") finish(false);
|
||||
});
|
||||
box.addEventListener("focusout", (e) => {
|
||||
if (!box.contains(e.relatedTarget as Node | null)) finish(true);
|
||||
});
|
||||
document.body.append(box);
|
||||
km.focus();
|
||||
km.select();
|
||||
}
|
||||
|
||||
/** 다이어그램 — refresh() 로 캐시 값 다시 그림 */
|
||||
export function createDiagram(
|
||||
projectId: string,
|
||||
@@ -71,7 +127,8 @@ export function createDiagram(
|
||||
const refresh = (): void => {
|
||||
const data = liveMilestone(projectId);
|
||||
const site = siteInfo();
|
||||
const layout = layoutMilestone(data, site, midKm(data, site?.mid_default_m ?? null));
|
||||
const mk = midKm(data, site?.mid_default_m ?? null);
|
||||
const layout = layoutMilestone(data, site, mk);
|
||||
const [vx, vy, vw, vh] = layout.view;
|
||||
// viewBox 원점 0 — 줌 · 팬 계산이 0 기준 · 그림은 안쪽 묶음에서 옮김
|
||||
const root = svg("svg", {
|
||||
@@ -126,7 +183,15 @@ export function createDiagram(
|
||||
const x = (ln.x1 + ln.x2) / 2;
|
||||
const y = (ln.y1 + ln.y2) / 2 - ((ln.labels.length - 1) * 16) / 2;
|
||||
const text = svg("text", { class: "b08-milestone__km", x, y, "text-anchor": "middle" });
|
||||
ln.labels.forEach((label, i) => text.append(svg("tspan", { x, dy: i ? 16 : 0 }, label)));
|
||||
ln.labels.forEach((label, i) => {
|
||||
const span = svg("tspan", { x, dy: i ? 16 : 0 }, label);
|
||||
span.dataset.b08MilestoneLeg = `${ln.from}:${i}`;
|
||||
span.addEventListener("dblclick", (e) => {
|
||||
e.stopPropagation(); // 원배율 되돌림(공용 더블클릭) 막음
|
||||
editLegAt(projectId, span, ln.from, i, mk);
|
||||
});
|
||||
text.append(span);
|
||||
});
|
||||
shift.append(text);
|
||||
}
|
||||
host.replaceChildren(root);
|
||||
|
||||
Reference in New Issue
Block a user