- 시점 창 · 구간 창 · 다이어그램 선 위 km 칸 · 자동 값 글 = 같은 도우미(fmtKm) - 안 고친 칸은 반올림 값으로 덮지 않음 - 시험 test_261_1 추가 · test_255_1 기대값만 고침 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018jonfdxn1Xit9bdkNJpE4G
286 lines
12 KiB
TypeScript
286 lines
12 KiB
TypeScript
/* =============================================================================
|
||
* 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<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 = 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);
|
||
},
|
||
});
|
||
}
|