- 등록 모달 [+ 줄] · 같은 이름 합침 없앰 · 기준위치 = 항목 · 목적지 없음 · 「고침」 → 「수정」 - 다이어그램 · 표 폭 전체 · 틀 높이 고정 · 틀 비율 그림 범위 · 처음 배율 = 상자 범위 맞춤 - 카카오 지도 높이 어디서나 480px - 도로 구분 = 선 자리(거래처 → 기준점 2차포장 · 기준점 → 시점 1차포장 · 그 밖 포장 · 비포장만 고름) · 파이썬 · TS 짝 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LLCk2y5xE8YGVgjbTntWsY
260 lines
10 KiB
TypeScript
260 lines
10 KiB
TypeScript
/* =============================================================================
|
||
* B08_Quantity_UI_Milestone_LegModal.ts
|
||
* 거리이정표 연결선 모달(PLAN 244-13 · 사용자 147번 8) — 다이어그램 선 · 선 글 누름.
|
||
*
|
||
* 값 = 도로 구분 ▾ · 거리(km) 구간 줄(빈칸 = [길찾기] 자동 값 · 사용자 값 우선 · 244-7)
|
||
* 도로 구분(152) = 포장 종류는 선 자리로 정함 — 거래처 → 기준점 = 2차포장 · 기준점 → 시점 = 1차포장 ·
|
||
* 기준점 없이 거래처 → 시점 · 시점 → 중점 = 포장 · ▾ 는 그 포장 · 비포장 둘(`roadOptions` · `roadOf`)
|
||
* 아래 = 다음(카카오) 지도 경로(그 구간 두 끝 · 마지막 길찾기 길 선 · 없으면 열 때 한 번 받음 · 자동 그림 없음)
|
||
* 거래처 · 기준점 표지 끌어 옮김 = 소재지 고침(좌표 → VWorld 주소) + 그 구간 다시 길찾기 · 시점 = 노선(고정)
|
||
* 기준점 → 시점 = 도로가 안 이어질 수 있음 — 거리 손 입력 · 시점 → 중점 = 늘 손 입력(길찾기 · 지도 없음)
|
||
* 자동 그림(244-11) = 거래처 쪽 구간 [길찾기] · 옮김 때만(`findSegmentKm`)
|
||
* ========================================================================== */
|
||
|
||
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 { chainVia, segmentEnds, setLeg } from "./B08_Quantity_Milestone_Layout";
|
||
import { editMap, type MapPin } from "./B08_Quantity_UI_Milestone_KakaoMap";
|
||
import { toastKm } from "./B08_Quantity_UI_Milestone_Modal";
|
||
import { reverseAddress } from "./B08_Quantity_UI_Milestone_Place";
|
||
import {
|
||
findSegmentKm,
|
||
liveMilestone,
|
||
putMilestone,
|
||
roadOptions,
|
||
segmentPath,
|
||
siteInfo,
|
||
} 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 }> = [];
|
||
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 ? "" : String(leg.km);
|
||
const auto = leg.auto_km ?? null;
|
||
if (auto != null) {
|
||
dist.input.placeholder = String(auto); // 빈칸 = 자동 값 · 고치면 그 값 우선
|
||
dist.input.title = `자동 값 ${auto} km`;
|
||
}
|
||
const ref = { road: road.select, km: dist.input, auto };
|
||
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 }) => ({
|
||
road: (ROADS as readonly string[]).includes(road.value) ? (road.value as Road) : null,
|
||
km: 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 ? "" : String(data.site.mid_station_m / 1000);
|
||
dist.input.placeholder = site?.mid_default_m == null ? "" : String(site.mid_default_m / 1000); // 빈칸 = 노선 ÷ 2
|
||
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,
|
||
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(거래처 → 기준점 · 시점) · "base"(기준점 → 시점) · "start"(시점 → 중점) */
|
||
export function openLegInfo(projectId: string, from: string): void {
|
||
if (from === "start") return openMidLeg(projectId);
|
||
const start = (): [number, number] | null => siteInfo()?.start_lonlat ?? null;
|
||
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[] => {
|
||
const d = liveMilestone(projectId);
|
||
return (from === "base" ? d.base.legs : d.routes.find((r) => r.id === from)?.legs) ?? [];
|
||
};
|
||
const via = chainVia(liveMilestone(projectId), 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 = from === "base" ? fresh.base : fresh.routes.find((r) => r.id === 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;
|
||
};
|
||
const pins = (): MapPin[] => {
|
||
const d = liveMilestone(projectId);
|
||
const route = d.routes.find((r) => r.id === 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 (route) add(route.id, route.name || "거래처", route.lon, route.lat);
|
||
if (from === "base" || via) add("base", d.base.name || "기준점", d.base.lon, d.base.lat);
|
||
else add("start", "시점", start()?.[0], start()?.[1]);
|
||
if (from === "base") 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<void> => {
|
||
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 = key === "base" ? fresh.base : fresh.routes.find((r) => r.id === 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);
|
||
body.append(
|
||
legs.root,
|
||
hint(
|
||
from === "base"
|
||
? "기준점 → 시점 = 도로가 안 이어질 수 있음 — 거리를 손으로 넣어도 됨(빈칸 = 길찾기 자동 값)"
|
||
: "빈칸 = 길찾기 자동 값 · 넣은 값이 우선",
|
||
),
|
||
status,
|
||
el("h4", { text: "경로 — 다음(카카오) 지도" }),
|
||
map.root,
|
||
);
|
||
drawMap();
|
||
// 길 선이 아직 없으면 열 때 한 번(km 같으면 안 씀 · 자동 그림 없음)
|
||
if (!segmentPath(from).length && seg.from && seg.to) void run(false);
|
||
},
|
||
});
|
||
}
|