/* ============================================================================= * B08_Quantity_UI_Milestone_LegModal.ts * 거리이정표 연결선 모달(PLAN 244-13 · 사용자 147번 8) — 다이어그램 선 · 선 글 누름. * * 값 = 도로 구분 ▾ · 거리(km) 구간 줄(빈칸 = [길찾기] 자동 값 · 사용자 값 우선 · 244-7) * 도로 구분(152 · 254) = 포장 종류는 선 자리로 정함 — → 기준점 = 2차포장 · 기준점 → 시점 = 1차포장 · * 거래처 → 시점 · 시점 → 중점 = 포장 · ▾ 는 그 포장 · 비포장 둘(`roadOptions` · `roadOf`) * 아래 = 다음(카카오) 지도 경로(그 구간 두 끝 · 마지막 길찾기 길 선 · 없으면 열 때 한 번 받음 · 자동 그림 없음) * 거래처 · 기준점 표지 끌어 옮김 = 소재지 고침(좌표 → VWorld 주소) + 그 구간 다시 길찾기 · 시점 = 노선(고정) * 기준점 → 시점 = 도로가 안 이어질 수 있음 — 거리 손 입력 · 시점 → 중점 = 늘 손 입력(길찾기 · 지도 없음) * 자동 그림(244-11) = 거래처 쪽 구간 [길찾기] · 옮김 때만(`findSegmentKm`) * ========================================================================== */ import { createCheckbox } from "@ui/ui_template_checkbox"; import { createButton, createInputField, createSelectField, el } from "@ui/ui_template_elements"; import { openModal } from "@ui/ui_template_modal"; import { ROADS, roadOf, segKind, type Leg, type Road, type SegKind, } from "./B08_Quantity_Milestone_Calc"; import { ownerOf, segmentEnds, setLeg } from "./B08_Quantity_Milestone_Layout"; import { editMap, type MapPin } from "./B08_Quantity_UI_Milestone_KakaoMap"; import { fmtKm, toastKm } from "./B08_Quantity_UI_Milestone_Modal"; import { reverseAddress } from "./B08_Quantity_UI_Milestone_Place"; import { findSegmentKm, liveMilestone, putMilestone, roadOptions, segmentPath, siteInfo, startPoint, } from "./B08_Quantity_UI_Milestone_State"; const km = (text: string): number | null => { const n = Number(text); return text.trim() && Number.isFinite(n) && n >= 0 ? n : null; }; /** 구간 줄 — 도로 구분 ▾(빈 값 = 안 고름) · 거리 km(빈칸 = 자동 값) · [×] · [+ 구간] */ export function legsEditor( initial: Leg[], kind: SegKind, ): { root: HTMLElement; value: () => Leg[] } { const list = el("div"); const rows: Array<{ road: HTMLSelectElement; km: HTMLInputElement; auto: number | null; orig: number | null; // 안 고친 칸 = 원래 값 그대로(반올림 값으로 안 덮음) shown: string; }> = []; const add = (leg: Leg): void => { const road = createSelectField({ label: "도로 구분", options: roadOptions(kind), // 포장 종류 = 선 자리(152) · 비포장만 고름 compact: true, }); const dist = createInputField({ label: "거리 (km)", type: "number", min: 0, compact: true }); road.select.value = roadOf(leg.road, kind); dist.input.value = leg.km == null ? "" : fmtKm(leg.km); const auto = leg.auto_km ?? null; if (auto != null) { dist.input.placeholder = fmtKm(auto); // 빈칸 = 자동 값 · 고치면 그 값 우선 dist.input.title = `자동 값 ${fmtKm(auto)} km`; } const ref = { road: road.select, km: dist.input, auto, orig: leg.km, shown: dist.input.value }; rows.push(ref); const drop = createButton({ label: "×", size: "sm", variant: "ghost", onClick: () => { rows.splice(rows.indexOf(ref), 1); row.remove(); }, }); const row = el("div", { className: "b08-milestone__line-edit", children: [road.root, dist.root, drop], }); row.dataset.b08MilestoneLeg = ""; list.append(row); }; (initial.length ? initial : [{ road: null, km: null }]).forEach(add); const more = createButton({ label: "+ 구간", size: "sm", variant: "ghost", onClick: () => add({ road: null, km: null }), }); return { root: el("div", { children: [list, more] }), value: () => rows.map(({ road, km: dist, auto, orig, shown }) => ({ road: (ROADS as readonly string[]).includes(road.value) ? (road.value as Road) : null, km: dist.value === shown ? orig : km(dist.value), auto_km: auto, })), }; } const hint = (text: string): HTMLElement => el("p", { className: "b08-options__hint", text }); /** 시점 → 중점 — 도로 구분 · 거리 늘 손 입력(빈칸 = 노선 ÷ 2) */ function openMidLeg(projectId: string): void { const data = liveMilestone(projectId); const site = siteInfo(); openModal({ title: "구간 — 시점 → 중점", closeLabel: "닫기", dialogClass: "b08-milestone__modal", mount: (body, close, actions) => { const road = createSelectField({ label: "도로 구분", options: roadOptions("mid"), compact: true, }); road.select.value = roadOf(data.site.mid_road, "mid"); const dist = createInputField({ label: "거리 (km)", type: "number", min: 0, compact: true }); dist.input.value = data.site.mid_station_m == null ? "" : fmtKm(data.site.mid_station_m / 1000); dist.input.placeholder = site?.mid_default_m == null ? "" : fmtKm(site.mid_default_m / 1000); // 빈칸 = 노선 ÷ 2 const shown = dist.input.value; // 안 고치면 저장 값(m) 그대로 dist.input.dataset.b08MilestoneLegKm = "start"; const save = createButton({ label: "수정", variant: "filled", onClick: () => { const fresh = liveMilestone(projectId); putMilestone( projectId, setLeg(fresh, "start", 0, { road: road.select.value as Road, ...(dist.input.value === shown ? {} : { km: km(dist.input.value) }), }), ); close(); }, }); save.dataset.b08MilestoneLegSave = ""; actions.append(save); body.append( el("div", { className: "b08-milestone__grid", children: [road.root, dist.root] }), hint("시점 → 중점 = 늘 손 입력(길찾기 없음) · 빈칸 = 노선 ÷ 2"), ); }, }); } /** 연결선 누름 — from = 거래처 · 기준점 id(→ 기준점 · 시점 · 254) · "start"(시점 → 중점) */ export function openLegInfo(projectId: string, from: string): void { if (from === "start") return openMidLeg(projectId); const start = (): [number, number] | null => startPoint(liveMilestone(projectId)); // 255-2 덮개 const seg = segmentEnds(liveMilestone(projectId), from, start()); if (!seg) return; openModal({ title: `구간 — ${seg.text}`, closeLabel: "닫기", dialogClass: "b08-milestone__modal", mount: (body, close, actions) => { const legsOf = (): Leg[] => ownerOf(liveMilestone(projectId), from)?.legs ?? []; const isBase = liveMilestone(projectId).bases.some((b) => b.id === from); const kind = segKind(liveMilestone(projectId), from); let legs = legsEditor(legsOf(), kind); const status = hint(""); status.dataset.b08MilestoneLegStatus = ""; /** 모달 구간 값 → 캐시(길찾기 · 옮김 앞) */ const apply = (): void => { const fresh = liveMilestone(projectId); const target = ownerOf(fresh, from); if (!target) return; target.legs = legs.value(); putMilestone(projectId, fresh); }; const redrawLegs = (): void => { const next = legsEditor(legsOf(), kind); legs.root.replaceWith(next.root); legs = next; }; /** 표지 = 그 구간 두 끝(주인 · to = 기준점 또는 시점) */ const pins = (): MapPin[] => { const d = liveMilestone(projectId); const owner = ownerOf(d, from); const out: MapPin[] = []; const add = (key: string, name: string, lon?: number | null, lat?: number | null): void => { if (lon != null && lat != null) out.push({ key, name, lon, lat, movable: key !== "start" }); }; if (!owner) return out; add(owner.id, owner.name || (isBase ? "기준점" : "거래처"), owner.lon, owner.lat); const target = d.bases.find((b) => b.id === owner.to); if (target) add(target.id, target.name || "기준점", target.lon, target.lat); else add("start", "시점", start()?.[0], start()?.[1]); return out; }; const drawMap = (): void => { const path = segmentPath(from); map.set(pins(), path.length >= 2 ? [path] : []); }; const run = async (image = true): Promise => { find.disabled = true; status.textContent = "길찾기 중…"; const typed = JSON.stringify(legs.value()); // 기다리는 동안 손댄 칸은 그대로 const text = await findSegmentKm(projectId, from, image).finally( () => (find.disabled = false), ); status.textContent = text; if (image) toastKm(text); if (JSON.stringify(legs.value()) === typed) redrawLegs(); drawMap(); }; // 끝점 끌어 옮김 = 그 소재지 고침(주소 다시) + 다시 길찾기 const map = editMap(projectId, { hint: "경로 = 카카오 길찾기 · 거래처 · 기준점 표지를 끌어 옮기면 다시 길찾기", onMove: (key, lon, lat) => { apply(); void reverseAddress(projectId, lon, lat).then((address) => { const fresh = liveMilestone(projectId); const target = ownerOf(fresh, key); if (!target) return; Object.assign(target, { lon, lat, address }); putMilestone(projectId, fresh); void run(); }); }, }); const find = createButton({ label: "길찾기", variant: "ghost", onClick: () => { apply(); void run(); }, }); find.dataset.b08MilestoneLegFind = ""; const save = createButton({ label: "수정", variant: "filled", onClick: () => { apply(); close(); }, }); save.dataset.b08MilestoneLegSave = ""; actions.append(find, save); // 155 [수기 입력] — 카카오 길찾기 연결 끊음(자동 값 지움 · 길찾기 · 지도 숨김 · km 손 입력) · 풀면 다시 길찾기 const owner = (d = liveMilestone(projectId)) => ownerOf(d, from); const manual = createCheckbox("수기 입력 — 카카오 길찾기 안 함(거리 손 입력)"); manual.input.checked = !!owner()?.manual; manual.input.dataset.b08MilestoneManual = ""; const auto = hint( isBase ? "기준점 구간 = 도로가 안 이어질 수 있음 — 거리를 손으로 넣어도 됨(빈칸 = 길찾기 자동 값)" : "빈칸 = 길찾기 자동 값 · 넣은 값이 우선", ); const mapHead = el("h4", { text: "경로 — 다음(카카오) 지도" }); const showMode = (): void => { const off = manual.input.checked; find.hidden = auto.hidden = mapHead.hidden = map.root.hidden = off; status.textContent = off ? "수기 입력 — 거리를 손으로 넣으세요" : status.textContent; }; manual.input.addEventListener("change", () => { const fresh = liveMilestone(projectId); const target = owner(fresh); if (!target) return; target.legs = legs.value().map((l) => (manual.input.checked ? { ...l, auto_km: null } : l)); target.manual = manual.input.checked; putMilestone(projectId, fresh); redrawLegs(); showMode(); if (!manual.input.checked) void run(); // 다시 카카오 연결 = 길찾기 한 번 }); body.append(manual.root, legs.root, auto, status, mapHead, map.root); showMode(); drawMap(); // 길 선이 아직 없으면 열 때 한 번(km 같으면 안 씀 · 자동 그림 없음 · 수기 입력이면 안 함) if (!manual.input.checked && !segmentPath(from).length && seg.from && seg.to) void run(false); }, }); }