merge: dev(254-1 여러 기준점) 합침 — 장소 그림 상자 id = bases id (256-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QND7Co43B24zK2P937rdxt
This commit is contained in:
@@ -118,7 +118,7 @@ def with_haul_routes(data: dict[str, Any], groups: list[dict[str, Any]]) -> dict
|
||||
"id": f"h-{g['key']}"[:40],
|
||||
"name": "",
|
||||
"address": "",
|
||||
"via_base": True,
|
||||
"to": "site",
|
||||
"linked": False,
|
||||
"dir": None,
|
||||
"haul": g["key"],
|
||||
|
||||
@@ -4,14 +4,16 @@
|
||||
거울 시험 `resources/tester/test_244_1_b08_milestone.py`.
|
||||
조작 중은 TS 짝이 바로 셈 · [저장] · 읽기 때 서버가 이 파일로 다시 셈.
|
||||
|
||||
표 한 줄 = 경로(업체) 구간 + (기준점 거침이면 기준점 → 시점 공통 구간) + (목적지 = 중점이면
|
||||
표 한 줄 = 경로(업체) 구간 + 기준점 사슬 구간들(254 · 거래처 `to` → 기준점 `to` → … → 시점 ·
|
||||
`base_chain` · 방문 집합으로 돌기 끊음 · 안 이은 기준점에서 멈춤) + (목적지 = 중점이면
|
||||
시점 → 중점 = 중점 측점 km) · 도로 구분 셋(2차포장 · 1차포장 · 비포장)마다 합 ·
|
||||
포장 = 2차 + 1차 · 비포장 · 합계 = 포장 + 비포장(견본 「거리이정표」 시트 수식) ·
|
||||
끝수 = 소수 셋째 자리(반올림).
|
||||
구간 km = 사용자 값 · 없으면 [길찾기] 자동 값(244-7).
|
||||
도로 구분(152) = 포장 종류는 선 자리로 정함(`road_of`) — 거래처 → 기준점 = 2차포장 · 기준점 → 시점 =
|
||||
1차포장 · 기준점 없이 거래처 → 시점 · 시점 → 중점 = 포장 · 사용자는 구간마다 비포장만 고름 ·
|
||||
포장 = 2차 + 1차 + 포장 · pick = 늘 거짓(옛 「도로 구분 고르기」 자리).
|
||||
도로 구분(152 · 254) = 포장 종류는 선 자리로 정함(`road_of`) — 시점에 닿는 기준점 구간 = 1차포장 ·
|
||||
그 밖 기준점 구간 · 거래처 → 기준점 = 2차포장 · 거래처 → 시점 · 시점 → 중점 = 포장 · 사용자는
|
||||
구간마다 비포장만 고름 · 포장 = 2차 + 1차 + 포장 · pick = 늘 거짓(옛 「도로 구분 고르기」 자리).
|
||||
옛 꼴(단일 `base` · 경로 `via_base`) = 읽을 때 `migrate_milestone` 로 새 꼴(`bases[]` · `to`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -21,8 +23,10 @@ from typing import Any
|
||||
|
||||
ROADS = ("2차포장", "1차포장", "포장", "비포장")
|
||||
KEYS = {"2차포장": "s2", "1차포장": "s1", "포장": "pv", "비포장": "un"}
|
||||
# 구간 자리 → 포장 종류(152) — via = 거래처 → 기준점 · base = 기준점 → 시점 · direct = 거래처 → 시점 · mid = 시점 → 중점
|
||||
# 구간 자리 → 포장 종류(152 · 254) — via = → 기준점 · base = 기준점 → 시점 ·
|
||||
# direct = 거래처 → 시점 · mid = 시점 → 중점
|
||||
PAVED = {"via": "2차포장", "base": "1차포장", "direct": "포장", "mid": "포장"}
|
||||
SITE = "site" # 이음 대상 = 시점(그 밖 = 기준점 id)
|
||||
|
||||
|
||||
def road_of(road: str | None, kind: str) -> str:
|
||||
@@ -55,21 +59,64 @@ def mid_km(data: dict[str, Any], mid_default_m: float | None) -> float:
|
||||
return float(station or 0) / 1000
|
||||
|
||||
|
||||
def base_chain(data: dict[str, Any], to: str) -> list[dict[str, Any]]:
|
||||
"""기준점 사슬(254) — to 부터 이은 기준점을 따라 시점까지 · 방문 집합(돌기 끊음) ·
|
||||
안 이은 · 없는 기준점에서 멈춤."""
|
||||
by_id = {b.get("id"): b for b in data.get("bases") or []}
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
while to != SITE and to not in seen:
|
||||
seen.add(to)
|
||||
base = by_id.get(to)
|
||||
if base is None or base.get("linked", True) is False:
|
||||
break
|
||||
out.append(base)
|
||||
to = base.get("to") or SITE
|
||||
return out
|
||||
|
||||
|
||||
def migrate_milestone(data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""옛 꼴 → 새 꼴(254) — 옛 `base`(켜짐) = bases[0] id "b1" ·
|
||||
경로 `via_base` = to "b1" | "site" · 새 꼴은 그대로(사본)."""
|
||||
rest = {k: v for k, v in data.items() if k != "base"}
|
||||
old = data.get("base") or {}
|
||||
fresh = isinstance(rest.get("bases"), list)
|
||||
on = not fresh and bool(old.get("enabled"))
|
||||
if fresh:
|
||||
bases = rest["bases"]
|
||||
elif on:
|
||||
keep = ("name", "address", "dir", "lon", "lat", "pos", "manual", "legs")
|
||||
bases = [{**{k: old[k] for k in keep if k in old}, "id": "b1", "to": SITE, "linked": True}]
|
||||
else:
|
||||
bases = []
|
||||
routes = []
|
||||
for route in rest.get("routes") or []:
|
||||
if not isinstance(route.get("to"), str):
|
||||
via = on and route.get("via_base", True) is not False
|
||||
route = {k: v for k, v in route.items() if k != "via_base"}
|
||||
route["to"] = "b1" if via else SITE
|
||||
routes.append(route)
|
||||
return {**rest, "bases": bases, "routes": routes}
|
||||
|
||||
|
||||
def _kind(owner: dict[str, Any], to_site: str, to_base: str) -> str:
|
||||
return to_site if (owner.get("to") or SITE) == SITE else to_base
|
||||
|
||||
|
||||
def compute_milestone(data: dict[str, Any], mid_default_m: float | None) -> list[dict[str, Any]]:
|
||||
"""표 줄 — 경로 차례 · 경로 안 줄 차례."""
|
||||
data = migrate_milestone(data)
|
||||
site = data.get("site") or {}
|
||||
base = data.get("base") or {}
|
||||
mid = {"road": site.get("mid_road") or "비포장", "km": mid_km(data, mid_default_m)}
|
||||
out: list[dict[str, Any]] = []
|
||||
for route in data.get("routes") or []:
|
||||
for index, row in enumerate(route.get("rows") or []):
|
||||
sums = {"s2": 0.0, "s1": 0.0, "pv": 0.0, "un": 0.0}
|
||||
linked = route.get("linked", True) is not False # 안 이은 거래처 = 구간 km 없음(155)
|
||||
via = linked and bool(base.get("enabled")) and route.get("via_base", True) is not False
|
||||
if linked:
|
||||
add_legs(sums, route.get("legs"), "via" if via else "direct")
|
||||
if via:
|
||||
add_legs(sums, base.get("legs"), "base")
|
||||
# 안 이은 거래처 = 구간 km 없음(155) · 이음 = 거래처 구간 + 기준점 사슬(254)
|
||||
if route.get("linked", True) is not False:
|
||||
add_legs(sums, route.get("legs"), _kind(route, "direct", "via"))
|
||||
for base in base_chain(data, route.get("to") or SITE):
|
||||
add_legs(sums, base.get("legs"), _kind(base, "base", "via"))
|
||||
if row.get("dest") == "mid":
|
||||
add_legs(sums, [mid], "mid")
|
||||
paved = sums["s2"] + sums["s1"] + sums["pv"]
|
||||
|
||||
@@ -6,20 +6,23 @@
|
||||
* 거울 시험 `resources/tester/test_244_1_b08_milestone.py`.
|
||||
* 조작 중은 이 파일이 바로 셈 · [저장] · 읽기 때 서버가 파이썬 짝으로 다시 셈.
|
||||
*
|
||||
* 표 한 줄 = 경로(업체) 구간 + (기준점 거침이면 기준점 → 시점 공통 구간) + (목적지 = 중점이면
|
||||
* 표 한 줄 = 경로(업체) 구간 + 기준점 사슬 구간들(254 · 거래처 `to` → 기준점 `to` → … → 시점 ·
|
||||
* `baseChain` · 방문 집합으로 돌기 끊음 · 안 이은 기준점에서 멈춤) + (목적지 = 중점이면
|
||||
* 시점 → 중점 = 중점 측점 km) · 도로 구분 셋마다 합 · 포장 = 2차 + 1차 · 합계 = 포장 + 비포장 ·
|
||||
* 끝수 = 소수 셋째 자리(반올림)
|
||||
* 구간 km = 사용자 값 · 없으면 [길찾기] 자동 값(244-7)
|
||||
* 도로 구분(152) = 포장 종류는 선 자리로 정함(`roadOf`) — 거래처 → 기준점 = 2차포장 · 기준점 → 시점 =
|
||||
* 1차포장 · 기준점 없이 거래처 → 시점 · 시점 → 중점 = 포장 · 사용자는 구간마다 비포장만 고름 ·
|
||||
* 포장 = 2차 + 1차 + 포장 · pick = 늘 거짓(옛 「도로 구분 고르기」 자리)
|
||||
* 도로 구분(152 · 254) = 포장 종류는 선 자리로 정함(`roadOf`) — 시점에 닿는 기준점 구간 = 1차포장 ·
|
||||
* 그 밖 기준점 구간 · 거래처 → 기준점 = 2차포장 · 거래처 → 시점 · 시점 → 중점 = 포장 · 사용자는
|
||||
* 구간마다 비포장만 고름 · 포장 = 2차 + 1차 + 포장 · pick = 늘 거짓(옛 「도로 구분 고르기」 자리)
|
||||
* 옛 꼴(단일 `base` · 경로 `via_base`) = 읽을 때 `migrateMilestone` 로 새 꼴(`bases[]` · `to`)
|
||||
* ========================================================================== */
|
||||
|
||||
export const ROADS = ["2차포장", "1차포장", "포장", "비포장"] as const;
|
||||
export type Road = (typeof ROADS)[number];
|
||||
/** 구간 자리(152) — 거래처 → 기준점(via) · 기준점 → 시점(base) · 거래처 → 시점(direct) · 시점 → 중점(mid) */
|
||||
/** 구간 자리(152 · 254) — 거래처 · 기준점 → 기준점(via) · 기준점 → 시점(base) · 거래처 → 시점(direct) ·
|
||||
* 시점 → 중점(mid) */
|
||||
export type SegKind = "via" | "base" | "direct" | "mid";
|
||||
/** 포장 종류 = 선 자리로 정함(152) — 기준점이 생기면 거래처 → 기준점 = 2차 · 기준점 → 시점 = 1차 · 없으면 포장 */
|
||||
/** 포장 종류 = 선 자리로 정함(152) — → 기준점 = 2차 · 기준점 → 시점 = 1차 · 거래처 → 시점 = 포장 */
|
||||
export const PAVED: Record<SegKind, Road> = {
|
||||
via: "2차포장",
|
||||
base: "1차포장",
|
||||
@@ -36,20 +39,81 @@ export function withRoads(data: MilestoneData): MilestoneData {
|
||||
return {
|
||||
...data,
|
||||
site: { ...data.site, mid_road: roadOf(data.site.mid_road, "mid") },
|
||||
base: { ...data.base, legs: fix(data.base.legs, "base") },
|
||||
routes: data.routes.map((r) => ({
|
||||
...r,
|
||||
legs: fix(r.legs, data.base.enabled && r.via_base !== false ? "via" : "direct"),
|
||||
})),
|
||||
bases: data.bases.map((b) => ({ ...b, legs: fix(b.legs, baseKind(b)) })),
|
||||
routes: data.routes.map((r) => ({ ...r, legs: fix(r.legs, routeKind(r)) })),
|
||||
};
|
||||
}
|
||||
|
||||
/** 선 자리 — from = 거래처 id · "base" · "start"(시점 → 중점) */
|
||||
/** 이음 대상 = 시점(254 · 그 밖 = 기준점 id) */
|
||||
export const SITE_ID = "site";
|
||||
const baseKind = (b: { to: string }): SegKind => (b.to === SITE_ID ? "base" : "via");
|
||||
const routeKind = (r: { to: string }): SegKind => (r.to === SITE_ID ? "direct" : "via");
|
||||
|
||||
/** 선 자리 — from = 거래처 id · 기준점 id · "start"(시점 → 중점) */
|
||||
export function segKind(data: MilestoneData, from: string): SegKind {
|
||||
if (from === "start") return "mid";
|
||||
if (from === "base") return "base";
|
||||
const base = data.bases.find((b) => b.id === from);
|
||||
if (base) return baseKind(base);
|
||||
const route = data.routes.find((r) => r.id === from);
|
||||
return data.base.enabled && route?.via_base !== false ? "via" : "direct";
|
||||
return route ? routeKind(route) : "direct";
|
||||
}
|
||||
|
||||
/** 기준점 사슬(254) — to 부터 이은 기준점을 따라 시점까지 · 방문 집합(돌기 끊음) · 안 이은 · 없는 기준점에서 멈춤 */
|
||||
export function baseChain(data: MilestoneData, to: string): MilestoneBase[] {
|
||||
const out: MilestoneBase[] = [];
|
||||
const seen = new Set<string>();
|
||||
let id = to;
|
||||
while (id !== SITE_ID && !seen.has(id)) {
|
||||
seen.add(id);
|
||||
const base = data.bases.find((b) => b.id === id);
|
||||
if (!base || base.linked === false) break;
|
||||
out.push(base);
|
||||
id = base.to;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
type OldBase = Partial<MilestoneBase> & { enabled?: boolean };
|
||||
type OldData = Partial<Omit<MilestoneData, "routes">> & {
|
||||
base?: OldBase | null;
|
||||
routes?: Array<Partial<MilestoneRoute> & { via_base?: boolean }>;
|
||||
};
|
||||
|
||||
/** 옛 꼴 → 새 꼴(254) — 옛 `base`(켜짐) = bases[0] id "b1" · 경로 `via_base` = to "b1" | "site" ·
|
||||
* 새 꼴은 그대로(사본) */
|
||||
export function migrateMilestone(data: OldData | MilestoneData): MilestoneData {
|
||||
const { base: old, ...rest } = data as OldData;
|
||||
const fresh = Array.isArray(rest.bases);
|
||||
const on = !fresh && !!old?.enabled;
|
||||
const bases: MilestoneBase[] = fresh
|
||||
? rest.bases!
|
||||
: on
|
||||
? [
|
||||
{
|
||||
id: "b1",
|
||||
name: old!.name ?? "",
|
||||
address: old!.address ?? "",
|
||||
dir: old!.dir ?? null,
|
||||
lon: old!.lon ?? null,
|
||||
lat: old!.lat ?? null,
|
||||
pos: old!.pos ?? null,
|
||||
manual: !!old!.manual,
|
||||
legs: old!.legs ?? [],
|
||||
to: SITE_ID,
|
||||
linked: true,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
const routes = (rest.routes ?? []).map((r) => {
|
||||
if (typeof r.to === "string") return r as MilestoneRoute;
|
||||
const { via_base, ...keep } = r;
|
||||
return { ...keep, to: on && via_base !== false ? "b1" : SITE_ID } as MilestoneRoute;
|
||||
});
|
||||
return {
|
||||
site: rest.site ?? { mid_station_m: null, mid_road: "비포장" },
|
||||
bases,
|
||||
routes,
|
||||
};
|
||||
}
|
||||
/** 8방 — 기준점(없으면 공사지) 기준 지도 방위 */
|
||||
export const DIRS = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] as const;
|
||||
@@ -76,8 +140,8 @@ export interface MilestoneRoute {
|
||||
/** 업체 이름 = 하위 탭 이름 */
|
||||
name: string;
|
||||
address: string;
|
||||
/** 기준점 거침(기준점 없으면 무시 · 시점으로 바로) */
|
||||
via_base: boolean;
|
||||
/** 잇는 곳(254) — "site"(시점으로 바로) · 기준점 id */
|
||||
to: string;
|
||||
dir: Dir | null;
|
||||
lon?: number | null;
|
||||
lat?: number | null;
|
||||
@@ -91,25 +155,32 @@ export interface MilestoneRoute {
|
||||
haul?: string | null;
|
||||
/** 단가연결(251 · dg · sg · logic 꼴 · 정본) */
|
||||
ref?: Record<string, unknown> | null;
|
||||
/** 업체 → 기준점(거침) · 시점 구간 */
|
||||
/** 업체 → to(기준점 · 시점) 구간 */
|
||||
legs: Leg[];
|
||||
rows: MilestoneRow[];
|
||||
}
|
||||
|
||||
/** 기준점(254 · 여럿) — 거래처가 거쳐 가는 자리 · to = 시점 또는 다른 기준점 */
|
||||
export interface MilestoneBase {
|
||||
id: string;
|
||||
name: string;
|
||||
address: string;
|
||||
dir: Dir | null;
|
||||
lon?: number | null;
|
||||
lat?: number | null;
|
||||
pos?: [number, number] | null;
|
||||
manual?: boolean;
|
||||
/** 이 기준점 → to 구간 */
|
||||
legs: Leg[];
|
||||
/** 잇는 곳 — "site"(시점) · 다른 기준점 id */
|
||||
to: string;
|
||||
/** false = 아직 선 안 이음(요소 추가 뒤) · 없음 = 이음 */
|
||||
linked?: boolean;
|
||||
}
|
||||
|
||||
export interface MilestoneData {
|
||||
site: { mid_station_m: number | null; mid_road: Road };
|
||||
base: {
|
||||
enabled: boolean;
|
||||
name: string;
|
||||
address: string;
|
||||
dir: Dir | null;
|
||||
lon?: number | null;
|
||||
lat?: number | null;
|
||||
pos?: [number, number] | null;
|
||||
manual?: boolean;
|
||||
/** 기준점 → 시점 공통 구간 */
|
||||
legs: Leg[];
|
||||
};
|
||||
bases: MilestoneBase[];
|
||||
routes: MilestoneRoute[];
|
||||
}
|
||||
|
||||
@@ -153,19 +224,20 @@ export function midKm(data: MilestoneData, midDefaultM: number | null): number {
|
||||
|
||||
/** 표 줄 — 경로 차례 · 경로 안 줄 차례 */
|
||||
export function computeMilestone(
|
||||
data: MilestoneData,
|
||||
raw: MilestoneData,
|
||||
midDefaultM: number | null,
|
||||
): MilestoneTableRow[] {
|
||||
const base = data.base;
|
||||
const data = migrateMilestone(raw);
|
||||
const mid: Leg = { road: data.site?.mid_road ?? "비포장", km: midKm(data, midDefaultM) };
|
||||
const out: MilestoneTableRow[] = [];
|
||||
for (const route of data.routes ?? []) {
|
||||
(route.rows ?? []).forEach((row, index) => {
|
||||
const sums = { s2: 0, s1: 0, pv: 0, un: 0 };
|
||||
const linked = route.linked !== false; // 안 이은 거래처 = 구간 km 없음(155)
|
||||
const via = linked && !!base?.enabled && route.via_base !== false;
|
||||
if (linked) addLegs(sums, route.legs, via ? "via" : "direct");
|
||||
if (via) addLegs(sums, base.legs, "base");
|
||||
if (route.linked !== false) {
|
||||
// 안 이은 거래처 = 구간 km 없음(155) · 이음 = 거래처 구간 + 기준점 사슬(254)
|
||||
addLegs(sums, route.legs, routeKind(route));
|
||||
for (const b of baseChain(data, route.to)) addLegs(sums, b.legs, baseKind(b));
|
||||
}
|
||||
if (row.dest === "mid") addLegs(sums, [mid], "mid");
|
||||
const paved = sums.s2 + sums.s1 + sums.pv;
|
||||
out.push({
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
* 다른 선 · 다른 글을 피한 자리(`placeLabels`) · 상자 안 글 = 안쪽 여백 넘으면 줄임표
|
||||
* ========================================================================== */
|
||||
|
||||
import type { Dir, Leg, MilestoneData } from "./B08_Quantity_Milestone_Calc";
|
||||
import type { Dir, Leg, MilestoneBase, MilestoneData } from "./B08_Quantity_Milestone_Calc";
|
||||
|
||||
export interface DiagramBox {
|
||||
id: string;
|
||||
@@ -201,7 +201,7 @@ export function setAutoKm(legs: Leg[], km: number): Leg[] {
|
||||
}
|
||||
|
||||
export interface KmSegment {
|
||||
/** route = 거래처 구간 · base = 기준점 → 시점 공통 구간 */
|
||||
/** route = 거래처 구간 · base = 기준점 구간(→ 시점 · 다른 기준점) */
|
||||
key: "route" | "base";
|
||||
text: string;
|
||||
from: [number, number] | null;
|
||||
@@ -211,40 +211,64 @@ export interface KmSegment {
|
||||
const pointOf = (p: { lon?: number | null; lat?: number | null }): [number, number] | null =>
|
||||
p.lon != null && p.lat != null ? [p.lon, p.lat] : null;
|
||||
|
||||
/** 구간 한 줄(244-13 연결선) — from = 거래처 id(거래처 → 기준점(거침) · 시점) · "base"(기준점 → 시점) ·
|
||||
* 없는 구간 = null(시점 → 중점 = 손 입력 · 길찾기 안 함) */
|
||||
/** 이음 대상 = 시점(Calc `SITE_ID` 와 같음 · 형 import 만) */
|
||||
const TO_SITE = "site";
|
||||
const baseOf = (data: MilestoneData, id: string): MilestoneBase | undefined =>
|
||||
data.bases.find((b) => b.id === id);
|
||||
/** 구간 주인 — 기준점 · 거래처(from id) */
|
||||
export const ownerOf = (data: MilestoneData, from: string) =>
|
||||
baseOf(data, from) ?? data.routes.find((r) => r.id === from);
|
||||
|
||||
/** 기준점 사슬(254 · Calc `baseChain` 과 같음) — to 부터 시점까지 · 돌기 · 안 이음 · 없음에서 멈춤 */
|
||||
export function chainOf(data: MilestoneData, to: string): MilestoneBase[] {
|
||||
const out: MilestoneBase[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (let id = to; id !== TO_SITE && !seen.has(id);) {
|
||||
seen.add(id);
|
||||
const base = baseOf(data, id);
|
||||
if (!base || base.linked === false) break;
|
||||
out.push(base);
|
||||
id = base.to;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 돌기(254) — 기준점 from 을 to 에 이으면 to 사슬이 from 으로 돌아옴(A → B → A · 이음 여부 무관) */
|
||||
export function makesLoop(data: MilestoneData, from: string, to: string): boolean {
|
||||
const seen = new Set<string>();
|
||||
for (let id = to; id !== TO_SITE && !seen.has(id); id = baseOf(data, id)?.to ?? TO_SITE) {
|
||||
if (id === from) return true;
|
||||
seen.add(id);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 구간 한 줄(244-13 연결선) — from = 거래처 id · 기준점 id(→ to = 시점 · 기준점) ·
|
||||
* 없는 구간 = null(시점 → 중점 = 손 입력 · 길찾기 안 함 · 안 이음) */
|
||||
export function segmentEnds(data: MilestoneData, from: string, start: LonLat): KmSegment | null {
|
||||
const baseName = data.base.name || "기준점";
|
||||
if (from === "base")
|
||||
return data.base.enabled
|
||||
? { key: "base", text: `${baseName} → 시점`, from: pointOf(data.base), to: start ?? null }
|
||||
: null;
|
||||
const route = data.routes.find((r) => r.id === from);
|
||||
if (!route || route.linked === false) return null; // 155 안 이은 거래처 = 구간 없음
|
||||
const viaBase = data.base.enabled && route.via_base;
|
||||
const base = baseOf(data, from);
|
||||
const owner = base ?? data.routes.find((r) => r.id === from);
|
||||
if (!owner || owner.linked === false) return null; // 155 안 이은 상자 = 구간 없음
|
||||
const target = owner.to === TO_SITE ? undefined : baseOf(data, owner.to);
|
||||
if (owner.to !== TO_SITE && !target) return null;
|
||||
return {
|
||||
key: "route",
|
||||
text: `${route.name || "거래처"} → ${viaBase ? baseName : "시점"}`,
|
||||
from: pointOf(route),
|
||||
to: viaBase ? pointOf(data.base) : (start ?? null),
|
||||
key: base ? "base" : "route",
|
||||
text: `${owner.name || (base ? "기준점" : "거래처")} → ${target ? target.name || "기준점" : "시점"}`,
|
||||
from: pointOf(owner),
|
||||
to: target ? pointOf(target) : (start ?? null),
|
||||
};
|
||||
}
|
||||
|
||||
/** 기준점 거쳐 시점 들어가는 구간(244-13 · 도로 구분 2차 · 1차 · 비포장) — 기준점 → 시점 · 거침 거래처 */
|
||||
export function chainVia(data: MilestoneData, from: string): boolean {
|
||||
if (!data.base.enabled || from === "start") return false;
|
||||
return from === "base" || !!data.routes.find((r) => r.id === from)?.via_base;
|
||||
}
|
||||
|
||||
/** [길찾기] 구간 — 거래처 → 기준점 · 기준점 → 시점(거침) · 기준점 없으면 거래처 → 시점
|
||||
/** [길찾기] 구간 — 거래처 → to · 그 뒤 기준점 사슬 구간들(254)
|
||||
* (시점 → 중점 = 노선 측점 거리 그대로 · 길찾기 안 함) */
|
||||
export function kmSegments(data: MilestoneData, routeId: string, start: LonLat): KmSegment[] {
|
||||
const first = segmentEnds(data, routeId, start);
|
||||
if (!first) return [];
|
||||
return chainVia(data, routeId) ? [first, segmentEnds(data, "base", start)!] : [first];
|
||||
const route = data.routes.find((r) => r.id === routeId);
|
||||
if (!first || !route) return [];
|
||||
return [first, ...chainOf(data, route.to).map((b) => segmentEnds(data, b.id, start)!)];
|
||||
}
|
||||
|
||||
/** 카카오 지도 표지(244-9) — 거래처 · 기준점(거침 · 좌표 있을 때) · 시점 */
|
||||
/** 카카오 지도 표지(244-9) — 거래처 · 기준점 사슬(좌표 있을 때) · 시점 */
|
||||
export function mapMarks(
|
||||
data: MilestoneData,
|
||||
routeId: string,
|
||||
@@ -257,19 +281,15 @@ export function mapMarks(
|
||||
if (p) out.push({ name, lon: p[0], lat: p[1] });
|
||||
};
|
||||
add(route.name || "거래처", pointOf(route));
|
||||
if (data.base.enabled && route.via_base) add(data.base.name || "기준점", pointOf(data.base));
|
||||
if (route.linked !== false)
|
||||
for (const b of chainOf(data, route.to)) add(b.name || "기준점", pointOf(b));
|
||||
add("시점", start ?? null);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 자동 km 를 그 구간에(사용자 km 는 그대로) */
|
||||
export function putAutoKm(
|
||||
data: MilestoneData,
|
||||
routeId: string,
|
||||
key: KmSegment["key"],
|
||||
km: number,
|
||||
): MilestoneData {
|
||||
const legs = key === "base" ? data.base.legs : data.routes.find((r) => r.id === routeId)?.legs;
|
||||
/** 자동 km 를 그 구간에(from = 거래처 · 기준점 id · 사용자 km 는 그대로) */
|
||||
export function putAutoKm(data: MilestoneData, from: string, km: number): MilestoneData {
|
||||
const legs = ownerOf(data, from)?.legs;
|
||||
if (legs) setAutoKm(legs, km);
|
||||
return data;
|
||||
}
|
||||
@@ -363,8 +383,7 @@ export function layoutMilestone(
|
||||
};
|
||||
/** 상자 놓기 — 상자 안 글 줄임(안쪽 여백) · out = 바깥 방향(겹치면 GAP 날 때까지 그쪽으로 밀어 냄) */
|
||||
// 153 사용자가 끌어 놓은 자리(왼 위 · 그림 좌표) — 있으면 방위 자리 · 밀어 냄 대신 그 자리
|
||||
const posOf = (id: string): [number, number] | null | undefined =>
|
||||
id === "base" ? data.base.pos : data.routes.find((r) => r.id === id)?.pos;
|
||||
const posOf = (id: string): [number, number] | null | undefined => ownerOf(data, id)?.pos;
|
||||
const put = (box: DiagramBox, out?: [number, number]): void => {
|
||||
const inner = box.w - 2 * INSET;
|
||||
box.label = clip(box.label, inner, FONT.label);
|
||||
@@ -411,31 +430,60 @@ export function layoutMilestone(
|
||||
line("start", "mid", [{ road: data.site.mid_road, km: Math.round(midKm * 1000) / 1000 }]);
|
||||
|
||||
const startLL = site?.start_lonlat ?? null;
|
||||
const useBase = data.base.enabled;
|
||||
const baseLL: LonLat =
|
||||
data.base.lon != null && data.base.lat != null ? [data.base.lon, data.base.lat] : null;
|
||||
let baseDeg = 270;
|
||||
if (useBase) {
|
||||
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;
|
||||
const llOf = (p?: { lon?: number | null; lat?: number | null }): LonLat =>
|
||||
p && p.lon != null && p.lat != null ? [p.lon, p.lat] : null;
|
||||
const used = new Set<string>();
|
||||
const keyOf = (anchor: string, deg: number): string => `${anchor}:${Math.round(deg) % 360}`;
|
||||
/** 상자 id → 잇는 곳에서 본 방위(기준점 둘레 빈 방위 = 그 방위에서 돌림 · 되돌아가는 쪽 뺌) */
|
||||
const degOf = new Map<string, number>();
|
||||
let auto = 0;
|
||||
/** 방위 안 정한 상자 — 빈 8방 차례(기준점 둘레 = 잇는 곳 반대편부터 · 잇는 곳으로 향하는 쪽 뺌) */
|
||||
const free = (anchor: string): number => {
|
||||
const turn = anchor === "site" ? 0 : (degOf.get(anchor) ?? 0);
|
||||
const blocked = anchor === "site" ? -1 : turn + 180;
|
||||
const cands = AUTO.map((d) => (DIR_DEG[d] + turn) % 360).filter(
|
||||
(c) => keyOf(anchor, c) !== keyOf(anchor, blocked),
|
||||
);
|
||||
return cands.find((c) => !used.has(keyOf(anchor, c))) ?? cands[auto++ % cands.length];
|
||||
};
|
||||
/** 잇는 곳 상자 — 놓인 기준점이면 그 id · 아니면 공사지 */
|
||||
const anchorOf = (to: string): string => (to !== "site" && byId.has(to) ? to : "site");
|
||||
|
||||
// 기준점(254 · 여럿) — 잇는 곳(시점 · 다른 기준점) 기준 방위(좌표 → 방위각 · 없으면 서부터 빈 방위) ·
|
||||
// 잇는 곳이 먼저 놓이게 사슬 차례 · 돌기 = 공사지 둘레
|
||||
const waiting = [...data.bases];
|
||||
while (waiting.length) {
|
||||
let i = waiting.findIndex(
|
||||
(b) => b.linked === false || b.to === "site" || byId.has(b.to) || !ownerOf(data, b.to),
|
||||
);
|
||||
if (i < 0) i = 0;
|
||||
const [b] = waiting.splice(i, 1);
|
||||
const anchor = b.linked === false ? "site" : anchorOf(b.to);
|
||||
const anchorLL = anchor === "site" ? startLL : (llOf(ownerOf(data, anchor)) ?? startLL);
|
||||
const deg =
|
||||
placeDeg({ dir: null, lon: b.lon, lat: b.lat }, anchorLL) ??
|
||||
(anchor === "site" && !used.has(keyOf("site", 270)) ? 270 : free(anchor));
|
||||
used.add(keyOf(anchor, deg));
|
||||
degOf.set(b.id, deg);
|
||||
const [ax, ay] = center(anchor);
|
||||
const [ux, uy] = unit(deg);
|
||||
put(
|
||||
{
|
||||
id: "base",
|
||||
id: b.id,
|
||||
kind: "base",
|
||||
x: cx - BOX.w / 2,
|
||||
y: cy - BOX.h / 2,
|
||||
x: ax + ux * R_BASE - BOX.w / 2,
|
||||
y: ay + uy * R_BASE - BOX.h / 2,
|
||||
w: BOX.w,
|
||||
h: BOX.h,
|
||||
label: data.base.name || "기준점",
|
||||
label: b.name || "기준점",
|
||||
},
|
||||
[ux, uy],
|
||||
);
|
||||
line("base", "start", data.base.legs);
|
||||
if (b.linked !== false && (b.to === "site" || byId.has(b.to)))
|
||||
line(b.id, b.to === "site" ? "start" : b.to, b.legs);
|
||||
}
|
||||
|
||||
// 거래처 — 기준점(거침) · 공사지 기준 방위 · 같은 방위끼리 옆으로 나란히
|
||||
// 거래처 — 잇는 곳(기준점 · 공사지) 기준 방위 · 같은 방위끼리 옆으로 나란히
|
||||
const groups = new Map<
|
||||
string,
|
||||
Array<{
|
||||
@@ -446,25 +494,15 @@ export function layoutMilestone(
|
||||
anchor: string;
|
||||
deg: number;
|
||||
legs: Leg[];
|
||||
linked: boolean;
|
||||
}>
|
||||
>();
|
||||
const used = new Set<string>();
|
||||
const keyOf = (anchor: string, deg: number): string => `${anchor}:${Math.round(deg) % 360}`;
|
||||
const items = data.routes.map((r) => {
|
||||
const anchor = useBase && r.via_base ? "base" : "site";
|
||||
return { r, anchor, deg: placeDeg(r, anchor === "base" ? (baseLL ?? startLL) : startLL) };
|
||||
const anchor = anchorOf(r.to);
|
||||
const anchorLL = anchor === "site" ? startLL : (llOf(ownerOf(data, anchor)) ?? startLL);
|
||||
return { r, anchor, deg: placeDeg(r, anchorLL) };
|
||||
});
|
||||
for (const it of items) if (it.deg !== null) used.add(keyOf(it.anchor, it.deg));
|
||||
let auto = 0;
|
||||
/** 방위 안 고른 거래처 — 빈 8방 차례(기준점 쪽 = 공사지 반대편부터 · 기준점 · 공사지로 향하는 쪽 뺌) */
|
||||
const free = (anchor: string): number => {
|
||||
const turn = anchor === "base" ? baseDeg : 0;
|
||||
const blocked = anchor === "base" ? baseDeg + 180 : useBase ? baseDeg : -1;
|
||||
const cands = AUTO.map((d) => (DIR_DEG[d] + turn) % 360).filter(
|
||||
(c) => keyOf(anchor, c) !== keyOf(anchor, blocked),
|
||||
);
|
||||
return cands.find((c) => !used.has(keyOf(anchor, c))) ?? cands[auto++ % cands.length];
|
||||
};
|
||||
for (const { r, anchor, deg } of items) {
|
||||
const d = deg ?? free(anchor);
|
||||
used.add(keyOf(anchor, d));
|
||||
@@ -479,11 +517,13 @@ export function layoutMilestone(
|
||||
anchor,
|
||||
deg: d,
|
||||
legs: r.legs,
|
||||
// 155 안 이은 거래처 = 선 없음(연결 점으로 이음) · 없는 기준점에 이음 = 선 없음
|
||||
linked: r.linked !== false && (r.to === "site" || anchor === r.to),
|
||||
});
|
||||
}
|
||||
for (const group of groups.values()) {
|
||||
group.forEach((g, k) => {
|
||||
const [ax, ay] = center(g.anchor === "base" ? "base" : "site");
|
||||
const [ax, ay] = center(g.anchor);
|
||||
const [ux, uy] = unit(g.deg);
|
||||
// 나란히 = 방위에 직각 · 상자 직각 폭 + 여유만큼(④)
|
||||
const spread = Math.abs(ux) * BOX.h + Math.abs(uy) * BOX.w + GAP;
|
||||
@@ -506,9 +546,7 @@ export function layoutMilestone(
|
||||
},
|
||||
[ux, uy],
|
||||
);
|
||||
// 155 안 이은 거래처 = 선 없음(연결 점으로 이음)
|
||||
if (data.routes.find((r) => r.id === g.id)?.linked !== false)
|
||||
line(g.id, g.anchor === "base" ? "base" : "start", g.legs);
|
||||
if (g.linked) line(g.id, g.anchor === "site" ? "start" : g.anchor, g.legs);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -530,11 +568,10 @@ export function layoutMilestone(
|
||||
};
|
||||
}
|
||||
|
||||
/** 선 글 한 줄 → 그 구간 — from = 거래처 id(거래처 구간) · "base"(공통 구간) · "start"(시점 → 중점) */
|
||||
/** 선 글 한 줄 → 그 구간 — from = 거래처 id · 기준점 id · "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;
|
||||
return ownerOf(data, from)?.legs[i] ?? null;
|
||||
}
|
||||
|
||||
/** 선 글에서 고친 값 → 데이터(시점 → 중점 km = 중점 측점 m) · 고친 것만 */
|
||||
@@ -550,8 +587,7 @@ export function setLeg(
|
||||
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];
|
||||
const leg = ownerOf(data, from)?.legs[i];
|
||||
if (leg) Object.assign(leg, patch);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ GET 답 `place_images` {상자 id: "place/<id>.png"} · `place_times` {상자 id
|
||||
공사지 = 시점(노선 첫 점 · 측점 0) · 중점 기본 = 노선 연장 ÷ 2 — 노선은 설계 노선 정본 차례
|
||||
(수정본 → 초기 폴리라인 → 예상노선 → 초기값 · `load_design_route` 와 같은 차례).
|
||||
표 합 = `B08_Quantity_Milestone_Calc`(파이썬 · TS 짝).
|
||||
기준점 = `bases[]`(254 · 여럿 · to = 시점 또는 다른 기준점) · 경로 `to` — 옛 `base` · `via_base` 는
|
||||
읽을 때 · 받을 때 새 꼴로 옮김(`migrate_milestone` · 저장 = 새 꼴).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -60,7 +62,7 @@ from B01_Dashboard.B01_Dashboard_Map import reverse_geocode
|
||||
from B02_ProjRegister.B02_ProjRegister_Repository import get_project_by_id
|
||||
from B03_FileInput.B03_FileInput_Engine import save_upload_stream
|
||||
from B08_Quantity.B08_Quantity_Haul import haul_groups, with_haul_routes
|
||||
from B08_Quantity.B08_Quantity_Milestone_Calc import compute_milestone, r3
|
||||
from B08_Quantity.B08_Quantity_Milestone_Calc import SITE, compute_milestone, migrate_milestone, r3
|
||||
from B08_Quantity.B08_Quantity_Milestone_Capture import make_place_image, make_route_image
|
||||
from B08_Quantity.B08_Quantity_Router_Hydro import _project_root
|
||||
from B08_Quantity.B08_Quantity_Router_Material import material_sheets
|
||||
@@ -83,6 +85,7 @@ PLACE = "place" # 장소 그림 하위 폴더(256-1)
|
||||
PRINTS = ".prints.json" # 그림 지문 {"place/<id>": 해시} — 바뀐 것만 다시 캡처
|
||||
IMAGE_EXT = (".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp")
|
||||
ROUTE_ID = r"^[A-Za-z0-9_-]{1,40}$"
|
||||
BOX_IDS = {SITE, "start", "mid"} # 다이어그램 공사지 상자 id — 기준점 · 경로 id 로 못 씀
|
||||
|
||||
Road = Literal["2차포장", "1차포장", "포장", "비포장"] # 포장 = 기준점 안 거친 구간(244-13)
|
||||
Dir = Literal["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
|
||||
@@ -114,7 +117,7 @@ class Place(BaseModel):
|
||||
|
||||
class Route(Place):
|
||||
id: str = Field(pattern=ROUTE_ID)
|
||||
via_base: bool = True
|
||||
to: str = Field(default=SITE, pattern=ROUTE_ID) # 254 잇는 곳 = "site"(시점) · 기준점 id
|
||||
# 251 운반 묶음 키(총괄자재 · B09 운반 줄과 이음) · 단가연결(dg · sg · logic 꼴 · 정본)
|
||||
haul: str | None = Field(default=None, max_length=60, pattern=r"^[A-Za-z0-9_-]+$")
|
||||
ref: dict[str, Any] | None = None
|
||||
@@ -123,7 +126,11 @@ class Route(Place):
|
||||
|
||||
|
||||
class Base(Place):
|
||||
enabled: bool = False
|
||||
"""기준점(254 · 여럿) — to = 시점 또는 다른 기준점 id(돌기는 화면이 막고 셈이 끊음)."""
|
||||
|
||||
id: str = Field(pattern=ROUTE_ID)
|
||||
to: str = Field(default=SITE, pattern=ROUTE_ID)
|
||||
linked: bool = True # False = 아직 선 안 이음(요소 추가 뒤)
|
||||
|
||||
|
||||
class Site(BaseModel):
|
||||
@@ -133,14 +140,22 @@ class Site(BaseModel):
|
||||
|
||||
class Milestone(BaseModel):
|
||||
site: Site = Field(default_factory=Site)
|
||||
base: Base = Field(default_factory=Base)
|
||||
bases: list[Base] = Field(default_factory=list, max_length=20)
|
||||
routes: list[Route] = Field(default_factory=list, max_length=100)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _migrate(cls, data: Any) -> Any:
|
||||
"""옛 꼴(단일 base · via_base) → 새 꼴(254)."""
|
||||
return migrate_milestone(data) if isinstance(data, dict) else data
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _unique_ids(self) -> Milestone:
|
||||
ids = [route.id for route in self.routes]
|
||||
ids = [b.id for b in self.bases] + [route.id for route in self.routes]
|
||||
if len(ids) != len(set(ids)):
|
||||
raise ValueError("경로 id 가 겹칩니다.")
|
||||
raise ValueError("경로 · 기준점 id 가 겹칩니다.")
|
||||
if BOX_IDS & set(ids):
|
||||
raise ValueError("site · start · mid 는 id 로 못 씁니다.")
|
||||
return self
|
||||
|
||||
|
||||
@@ -225,11 +240,11 @@ def images(root: str | Path, sub: str = "") -> dict[str, str]:
|
||||
|
||||
|
||||
def places(data: dict[str, Any]) -> list[tuple[str, str, float, float]]:
|
||||
"""좌표가 정해진 상자 → [(상자 id, 이름, 경도, 위도)] — 거래처 = 경로 id · 기준점 = "b1"."""
|
||||
# ponytail: 단일 기준점 — 254-1 bases[] 합친 뒤 bases[].id 로
|
||||
boxes = [(r["id"], r.get("name") or "", r.get("lon"), r.get("lat")) for r in data["routes"]]
|
||||
base = data.get("base") or {}
|
||||
boxes.append(("b1", base.get("name") or "기준점", base.get("lon"), base.get("lat")))
|
||||
"""좌표가 정해진 상자 → [(상자 id, 이름, 경도, 위도)] — 거래처 = 경로 id · 기준점 = bases id."""
|
||||
boxes = [
|
||||
(b["id"], b.get("name") or "", b.get("lon"), b.get("lat"))
|
||||
for b in [*data.get("routes", []), *data.get("bases", [])]
|
||||
]
|
||||
return [(i, n, lon, lat) for i, n, lon, lat in boxes if lon is not None and lat is not None]
|
||||
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ export function addRoute(projectId: string, init: Partial<MilestoneRoute> = {}):
|
||||
id,
|
||||
name: `거래처 ${data.routes.length + 1}`,
|
||||
address: "",
|
||||
via_base: true,
|
||||
to: "site",
|
||||
dir: null,
|
||||
legs: [{ road: null, km: null }], // 도로 구분 = 사용자 고름(244-7)
|
||||
rows: [{ item: "", dest: "start", note: "" }],
|
||||
@@ -168,12 +168,12 @@ function tableView(projectId: string): MilestoneView {
|
||||
const data = liveMilestone(projectId);
|
||||
const rows = computeMilestone(data, siteInfo()?.mid_default_m ?? null);
|
||||
// 기준점 생김 · 없앰 = 열 바뀜 → 표 새로
|
||||
const next = `${data.base.enabled}:${rows.some((r) => r.pv > 0)}`;
|
||||
const next = `${data.bases.length > 0}:${rows.some((r) => r.pv > 0)}`;
|
||||
if (next !== mode) {
|
||||
mode = next;
|
||||
const fresh = makeTable(
|
||||
milestoneColumns(
|
||||
data.base.enabled,
|
||||
data.bases.length > 0,
|
||||
rows.some((r) => r.pv > 0),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
import {
|
||||
layoutMilestone,
|
||||
legAt,
|
||||
ownerOf,
|
||||
panContent,
|
||||
panStart,
|
||||
setLeg,
|
||||
@@ -158,7 +159,7 @@ export function createDiagram(
|
||||
site: () => openSiteInfo(projectId),
|
||||
start: () => openSiteInfo(projectId),
|
||||
mid: () => openSiteInfo(projectId),
|
||||
base: () => openPlaceModal(projectId, { add: false, id: "base" }),
|
||||
base: (id) => openPlaceModal(projectId, { add: false, id }),
|
||||
};
|
||||
|
||||
/** 153 상자 끌어 옮김 — 4px 넘게 움직이면 끌기(누름 = 모달은 그대로) · 놓으면 그 자리 저장(캐시 · [저장] 때 서버)
|
||||
@@ -189,7 +190,7 @@ export function createDiagram(
|
||||
Math.round(box.y + (e.clientY - down.clientY) / k),
|
||||
];
|
||||
const fresh = liveMilestone(projectId);
|
||||
const target = box.id === "base" ? fresh.base : fresh.routes.find((r) => r.id === box.id);
|
||||
const target = ownerOf(fresh, box.id);
|
||||
if (!target) return;
|
||||
target.pos = pos;
|
||||
userZoomed = true; // 옮긴 뒤 배율 그대로
|
||||
@@ -200,8 +201,9 @@ export function createDiagram(
|
||||
});
|
||||
};
|
||||
|
||||
/** 154 연결 점 ● — 거래처 상자 오른쪽 가운데 · 끌어 기준점 상자 = 기준점 거침 · 공사지(시점 · 중점) = 시점으로 바로 ·
|
||||
* 빈 곳 · 같은 연결 = 취소 · 바뀌면 그 거래처 구간 새로(km 비움) + 길찾기 한 번(km · 경로 그림) */
|
||||
/** 154 연결 점 ● — 거래처 상자 오른쪽 가운데 · 끌어 기준점 상자 = 그 기준점 거침(254 · to = 기준점 id) ·
|
||||
* 공사지(시점 · 중점) = 시점으로 바로 · 빈 곳 · 같은 연결 = 취소 · 바뀌면 그 거래처 구간 새로(km 비움) +
|
||||
* 길찾기 한 번(km · 경로 그림) */
|
||||
const connectDot = (box: DiagramBox, shift: SVGGElement): SVGCircleElement => {
|
||||
const cx = box.x + box.w;
|
||||
const cy = box.y + box.h / 2;
|
||||
@@ -221,12 +223,13 @@ export function createDiagram(
|
||||
const wire = svg("line", { class: "b08-milestone__wire", x1: cx, y1: cy, x2: cx, y2: cy });
|
||||
shift.append(wire);
|
||||
let over: Element | null = null;
|
||||
const targetOf = (e: PointerEvent): "base" | "site" | null => {
|
||||
/** 놓을 곳 — 기준점 id · "site"(공사지 · 시점 · 중점) · 없음 */
|
||||
const targetOf = (e: PointerEvent): string | null => {
|
||||
const hit = document.elementFromPoint(e.clientX, e.clientY);
|
||||
const g = hit?.closest<SVGGElement>("[data-b08-milestone-box]");
|
||||
const id = g?.dataset.b08MilestoneBox;
|
||||
if (id === "base") return "base";
|
||||
return id === "site" || id === "start" || id === "mid" ? "site" : null;
|
||||
if (id === "site" || id === "start" || id === "mid") return "site";
|
||||
return id && liveMilestone(projectId).bases.some((b) => b.id === id) ? id : null;
|
||||
};
|
||||
const move = (e: PointerEvent): void => {
|
||||
const [x, y] = toLocal(e);
|
||||
@@ -249,10 +252,8 @@ export function createDiagram(
|
||||
const fresh = liveMilestone(projectId);
|
||||
const route = fresh.routes.find((r) => r.id === box.id);
|
||||
if (!t || !route) return;
|
||||
const via = t === "base";
|
||||
const now = route.linked === false ? null : fresh.base.enabled && route.via_base !== false;
|
||||
if (via === now) return; // 같은 연결 = 그대로
|
||||
route.via_base = via;
|
||||
if (route.linked !== false && route.to === t) return; // 같은 연결 = 그대로
|
||||
route.to = t;
|
||||
route.linked = true; // 155 요소 추가 뒤 첫 이음
|
||||
route.legs = [{ road: null, km: null }]; // 옛 구간 km = 다른 길 — 새로 길찾기
|
||||
putMilestone(projectId, fresh);
|
||||
@@ -270,7 +271,7 @@ export function createDiagram(
|
||||
variant: "ghost",
|
||||
onClick: () => {
|
||||
const fresh = liveMilestone(projectId);
|
||||
fresh.base.pos = null;
|
||||
fresh.bases.forEach((b) => (b.pos = null));
|
||||
fresh.routes.forEach((r) => (r.pos = null));
|
||||
userZoomed = false;
|
||||
putMilestone(projectId, fresh);
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* 거리이정표 연결선 모달(PLAN 244-13 · 사용자 147번 8) — 다이어그램 선 · 선 글 누름.
|
||||
*
|
||||
* 값 = 도로 구분 ▾ · 거리(km) 구간 줄(빈칸 = [길찾기] 자동 값 · 사용자 값 우선 · 244-7)
|
||||
* 도로 구분(152) = 포장 종류는 선 자리로 정함 — 거래처 → 기준점 = 2차포장 · 기준점 → 시점 = 1차포장 ·
|
||||
* 기준점 없이 거래처 → 시점 · 시점 → 중점 = 포장 · ▾ 는 그 포장 · 비포장 둘(`roadOptions` · `roadOf`)
|
||||
* 도로 구분(152 · 254) = 포장 종류는 선 자리로 정함 — → 기준점 = 2차포장 · 기준점 → 시점 = 1차포장 ·
|
||||
* 거래처 → 시점 · 시점 → 중점 = 포장 · ▾ 는 그 포장 · 비포장 둘(`roadOptions` · `roadOf`)
|
||||
* 아래 = 다음(카카오) 지도 경로(그 구간 두 끝 · 마지막 길찾기 길 선 · 없으면 열 때 한 번 받음 · 자동 그림 없음)
|
||||
* 거래처 · 기준점 표지 끌어 옮김 = 소재지 고침(좌표 → VWorld 주소) + 그 구간 다시 길찾기 · 시점 = 노선(고정)
|
||||
* 기준점 → 시점 = 도로가 안 이어질 수 있음 — 거리 손 입력 · 시점 → 중점 = 늘 손 입력(길찾기 · 지도 없음)
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
type Road,
|
||||
type SegKind,
|
||||
} from "./B08_Quantity_Milestone_Calc";
|
||||
import { chainVia, segmentEnds, setLeg } from "./B08_Quantity_Milestone_Layout";
|
||||
import { ownerOf, 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";
|
||||
@@ -144,7 +144,7 @@ function openMidLeg(projectId: string): void {
|
||||
});
|
||||
}
|
||||
|
||||
/** 연결선 누름 — from = 거래처 id(거래처 → 기준점 · 시점) · "base"(기준점 → 시점) · "start"(시점 → 중점) */
|
||||
/** 연결선 누름 — from = 거래처 · 기준점 id(→ 기준점 · 시점 · 254) · "start"(시점 → 중점) */
|
||||
export function openLegInfo(projectId: string, from: string): void {
|
||||
if (from === "start") return openMidLeg(projectId);
|
||||
const start = (): [number, number] | null => siteInfo()?.start_lonlat ?? null;
|
||||
@@ -155,11 +155,8 @@ export function openLegInfo(projectId: string, from: string): void {
|
||||
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 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("");
|
||||
@@ -167,7 +164,7 @@ export function openLegInfo(projectId: string, from: string): void {
|
||||
/** 모달 구간 값 → 캐시(길찾기 · 옮김 앞) */
|
||||
const apply = (): void => {
|
||||
const fresh = liveMilestone(projectId);
|
||||
const target = from === "base" ? fresh.base : fresh.routes.find((r) => r.id === from);
|
||||
const target = ownerOf(fresh, from);
|
||||
if (!target) return;
|
||||
target.legs = legs.value();
|
||||
putMilestone(projectId, fresh);
|
||||
@@ -177,18 +174,20 @@ export function openLegInfo(projectId: string, from: string): void {
|
||||
legs.root.replaceWith(next.root);
|
||||
legs = next;
|
||||
};
|
||||
/** 표지 = 그 구간 두 끝(주인 · to = 기준점 또는 시점) */
|
||||
const pins = (): MapPin[] => {
|
||||
const d = liveMilestone(projectId);
|
||||
const route = d.routes.find((r) => r.id === from);
|
||||
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 (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);
|
||||
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]);
|
||||
if (from === "base") add("start", "시점", start()?.[0], start()?.[1]);
|
||||
return out;
|
||||
};
|
||||
const drawMap = (): void => {
|
||||
@@ -214,7 +213,7 @@ export function openLegInfo(projectId: string, from: string): void {
|
||||
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);
|
||||
const target = ownerOf(fresh, key);
|
||||
if (!target) return;
|
||||
Object.assign(target, { lon, lat, address });
|
||||
putMilestone(projectId, fresh);
|
||||
@@ -242,14 +241,13 @@ export function openLegInfo(projectId: string, from: string): void {
|
||||
save.dataset.b08MilestoneLegSave = "";
|
||||
actions.append(find, save);
|
||||
// 155 [수기 입력] — 카카오 길찾기 연결 끊음(자동 값 지움 · 길찾기 · 지도 숨김 · km 손 입력) · 풀면 다시 길찾기
|
||||
const owner = (d = liveMilestone(projectId)) =>
|
||||
from === "base" ? d.base : d.routes.find((r) => r.id === from);
|
||||
const owner = (d = liveMilestone(projectId)) => ownerOf(d, from);
|
||||
const manual = createCheckbox("수기 입력 — 카카오 길찾기 안 함(거리 손 입력)");
|
||||
manual.input.checked = !!owner()?.manual;
|
||||
manual.input.dataset.b08MilestoneManual = "";
|
||||
const auto = hint(
|
||||
from === "base"
|
||||
? "기준점 → 시점 = 도로가 안 이어질 수 있음 — 거리를 손으로 넣어도 됨(빈칸 = 길찾기 자동 값)"
|
||||
isBase
|
||||
? "기준점 구간 = 도로가 안 이어질 수 있음 — 거리를 손으로 넣어도 됨(빈칸 = 길찾기 자동 값)"
|
||||
: "빈칸 = 길찾기 자동 값 · 넣은 값이 우선",
|
||||
);
|
||||
const mapHead = el("h4", { text: "경로 — 다음(카카오) 지도" });
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
* 상자 하나 = 항목 하나(149 · [+ 줄] 없음 · 같은 거래처라도 항목마다 따로 등록) · 경로 값은 연결선 모달
|
||||
* 연결(기준점 거침 · 시점으로 바로) = 다이어그램 연결 점 ● 끌기(154 · 155 · 등록 = 상자만 · 선 없음)
|
||||
* 열기 = 다이어그램 [요소 추가](155 · 공사지 · 기준점 [+] 없앰)
|
||||
* 분류 기준위치 = 기준점(하나 · 항목 · 목적지 없음 · 있으면 고르기 막음) · 방위 = 소재지 좌표로 자동 자리
|
||||
* 분류 기준위치 = 기준점(254 · 여럿 · 새 `bases` 원소 · 항목 · 목적지 없음 · 등록 = 상자만 · 연결 점으로 이음) ·
|
||||
* 방위 = 소재지 좌표로 자동 자리 · 기준점 [삭제] = 그 기준점에 이은 거래처 · 기준점은 안 이음으로
|
||||
* 구간 km · 도로 구분 · 길찾기 = 연결선 모달(`…_UI_Milestone_LegModal`) · [등록] · 소재지 옮김 뒤 그 구간 한 번
|
||||
* 시점(공사지) 상자 = 프로젝트 등록 주소 · VWorld 주소(좌표 → 주소 · 서버) · 아래 지도
|
||||
* 조작 = 캐시 · [저장] 때 서버
|
||||
@@ -87,20 +88,21 @@ function rowField(r: MilestoneRow, kind: "route" | "base") {
|
||||
|
||||
export type PlaceTarget = { add: true } | { add: false; id: string };
|
||||
|
||||
/** 등록 모달 — 추가([+]) · 고침(거래처 · 기준점 상자) 한 벌 · id "base" = 기준점 */
|
||||
/** 등록 모달 — 추가([+]) · 고침(거래처 · 기준점 상자) 한 벌 · id = 거래처 · 기준점 id */
|
||||
export function openPlaceModal(
|
||||
projectId: string,
|
||||
target: PlaceTarget,
|
||||
addRoute?: (init: Partial<MilestoneRoute>) => string,
|
||||
): void {
|
||||
const data = liveMilestone(projectId);
|
||||
const editBase = !target.add && target.id === "base";
|
||||
const base = target.add ? undefined : data.bases.find((b) => b.id === target.id);
|
||||
const editBase = !!base;
|
||||
const route = !target.add && !editBase ? data.routes.find((r) => r.id === target.id) : undefined;
|
||||
if (!target.add && !editBase && !route) return;
|
||||
const title = target.add
|
||||
? "요소 추가 — 기준점 · 거래처"
|
||||
: editBase
|
||||
? `기준점 — ${data.base.name || "기준점"}`
|
||||
: base
|
||||
? `기준점 — ${base.name || "기준점"}`
|
||||
: `항목 정보 — ${route!.name || "거래처"}`;
|
||||
openModal({
|
||||
title,
|
||||
@@ -118,20 +120,14 @@ export function openPlaceModal(
|
||||
});
|
||||
sort.select.value = kind;
|
||||
sort.select.disabled = !target.add;
|
||||
// 기준점 = 하나 — 이미 있으면 [+]에서 기준위치 못 고름(기준점 상자에서 고침)
|
||||
const baseOption = sort.select.querySelector<HTMLOptionElement>('option[value="base"]');
|
||||
if (target.add && data.base.enabled && baseOption) {
|
||||
baseOption.disabled = true;
|
||||
baseOption.textContent = "기준위치(있음 — 기준점 상자에서 수정)";
|
||||
}
|
||||
sort.select.dataset.b08MilestoneAdd = "sort";
|
||||
const name = createInputField({ label: "거래처 이름", compact: true });
|
||||
name.input.value = editBase ? data.base.name : (route?.name ?? "");
|
||||
name.input.value = base ? base.name : (route?.name ?? "");
|
||||
name.input.dataset.b08MilestoneAdd = "company";
|
||||
const nameLabel = name.root.querySelector(".ui-field__label");
|
||||
// 상자 하나 = 항목 하나(149) — 옛 자료의 둘째 줄부터는 손대지 않고 그대로 둠
|
||||
const first = rowField(route?.rows[0] ?? { item: "", dest: "start", note: "" }, kind);
|
||||
const old = editBase ? data.base : route;
|
||||
const old = base ?? route;
|
||||
const place = placePicker(
|
||||
projectId,
|
||||
{ address: old?.address ?? "", lon: old?.lon ?? null, lat: old?.lat ?? null },
|
||||
@@ -163,11 +159,24 @@ export function openPlaceModal(
|
||||
const where = place.value();
|
||||
const fresh = liveMilestone(projectId);
|
||||
let segment: string | undefined; // [등록] · 소재지 옮김 뒤 그 구간 길찾기 한 번
|
||||
if (kind === "base") {
|
||||
Object.assign(fresh.base, where, { enabled: true, name: text, dir: null });
|
||||
if (!fresh.base.legs.length) fresh.base.legs = [{ road: null, km: null }];
|
||||
if (kind === "base" && base) {
|
||||
const b = fresh.bases.find((x) => x.id === base.id);
|
||||
if (!b) return close();
|
||||
Object.assign(b, where, { name: text, dir: null });
|
||||
putMilestone(projectId, fresh);
|
||||
if (moved(where)) segment = b.id;
|
||||
} else if (kind === "base") {
|
||||
// 254 새 기준점 = 상자만 · 선은 연결 점 ● 로 이음(이을 때 길찾기)
|
||||
fresh.bases.push({
|
||||
id: `b${Date.now().toString(36)}${Math.floor(Math.random() * 1296).toString(36)}`,
|
||||
name: text,
|
||||
dir: null,
|
||||
legs: [{ road: null, km: null }],
|
||||
to: "site",
|
||||
linked: false,
|
||||
...where,
|
||||
});
|
||||
putMilestone(projectId, fresh);
|
||||
if (moved(where)) segment = "base";
|
||||
} else if (route) {
|
||||
const r = fresh.routes.find((x) => x.id === route.id);
|
||||
if (!r) return close();
|
||||
@@ -180,9 +189,9 @@ export function openPlaceModal(
|
||||
} else {
|
||||
if (!text) return name.setError("거래처 이름을 넣으세요.");
|
||||
// 155 등록 = 상자만 · 선은 다이어그램 연결 점 ● 로 이음(이을 때 길찾기)
|
||||
const id = addRoute?.({
|
||||
addRoute?.({
|
||||
name: text,
|
||||
via_base: true,
|
||||
to: "site",
|
||||
linked: false,
|
||||
dir: null,
|
||||
rows: rowValues(),
|
||||
@@ -198,12 +207,17 @@ export function openPlaceModal(
|
||||
label: "삭제",
|
||||
variant: "danger",
|
||||
onClick: async () => {
|
||||
const label = editBase ? data.base.name || "기준점" : route?.name || "거래처";
|
||||
const label = base ? base.name || "기준점" : route?.name || "거래처";
|
||||
if (!(await showConfirmDialog(`「${label}」 항목을 지울까요? — [저장] 때 반영`, "삭제")))
|
||||
return;
|
||||
const fresh = liveMilestone(projectId);
|
||||
if (editBase) fresh.base = { enabled: false, name: "", address: "", dir: null, legs: [] };
|
||||
else fresh.routes = fresh.routes.filter((r) => r.id !== route!.id); // 경로 탭도 같이
|
||||
if (base) {
|
||||
fresh.bases = fresh.bases.filter((b) => b.id !== base.id);
|
||||
// 254 그 기준점에 이은 상자 = 안 이음(옛 구간 km = 다른 길 · 연결 점으로 다시 이음)
|
||||
for (const o of [...fresh.bases, ...fresh.routes])
|
||||
if (o.to === base.id)
|
||||
Object.assign(o, { to: "site", linked: false, legs: [{ road: null, km: null }] });
|
||||
} else fresh.routes = fresh.routes.filter((r) => r.id !== route!.id); // 경로 탭도 같이
|
||||
putMilestone(projectId, fresh);
|
||||
close();
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "@ui/ui_template_context_menu";
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import type { Dest, MilestoneData, MilestoneRoute } from "./B08_Quantity_Milestone_Calc";
|
||||
import { chainOf, ownerOf } from "./B08_Quantity_Milestone_Layout";
|
||||
import { editMap, type MapPin } from "./B08_Quantity_UI_Milestone_KakaoMap";
|
||||
import { reverseAddress } from "./B08_Quantity_UI_Milestone_Place";
|
||||
import {
|
||||
@@ -131,28 +132,26 @@ export function routeView(projectId: string, routeId: string): MilestoneView {
|
||||
if (!r) return out;
|
||||
add(r.id, r.name || "거래처", r.lon, r.lat);
|
||||
if (r.linked === false) return out;
|
||||
if (d.base.enabled && r.via_base) add("base", d.base.name || "기준점", d.base.lon, d.base.lat);
|
||||
for (const b of chainOf(d, r.to)) add(b.id, b.name || "기준점", b.lon, b.lat); // 254 기준점 사슬
|
||||
add("start", "시점", start()?.[0], start()?.[1]);
|
||||
return out;
|
||||
};
|
||||
const lines = (): Array<Array<[number, number]>> => {
|
||||
const d = liveMilestone(projectId);
|
||||
const r = d.routes.find((x) => x.id === routeId);
|
||||
const via = !!r && r.linked !== false && d.base.enabled && r.via_base;
|
||||
return [segmentPath(routeId), ...(via ? [segmentPath("base")] : [])].filter(
|
||||
(p) => p.length >= 2,
|
||||
);
|
||||
const chain = r && r.linked !== false ? chainOf(d, r.to).map((b) => b.id) : [];
|
||||
return [routeId, ...chain].map(segmentPath).filter((p) => p.length >= 2);
|
||||
};
|
||||
const map = editMap(projectId, {
|
||||
hint: "",
|
||||
onMove: (key, lon, lat) =>
|
||||
void reverseAddress(projectId, lon, lat).then((addr) => {
|
||||
const fresh = liveMilestone(projectId);
|
||||
const target = key === "base" ? fresh.base : fresh.routes.find((x) => x.id === key);
|
||||
const target = ownerOf(fresh, key);
|
||||
if (!target) return;
|
||||
Object.assign(target, { lon, lat, address: addr });
|
||||
putMilestone(projectId, fresh);
|
||||
void findSegmentKm(projectId, key === "base" ? "base" : routeId).then(() => drawMap());
|
||||
void findSegmentKm(projectId, key).then(() => drawMap());
|
||||
}),
|
||||
});
|
||||
const drawMap = (): void => map.set(pins(), lines());
|
||||
|
||||
@@ -11,18 +11,24 @@
|
||||
* 경로 좌표(244-9) = 마지막 [길찾기] 응답을 메모리에만(카카오 지도 길 선 · 새로 고침이면 사라짐)
|
||||
* 경로 그림 자동(244-11) = [길찾기] 뒤 길 선 · 표지를 서버 POST …/image/auto → 서버 헤드리스 캡처 그림
|
||||
* (뒤에서 · 화면 안 잠금) · 사용자 그림 우선 · 실패 = 까닭 한 줄(알림 · 경로 탭) · 좌표는 서버도 안 남김
|
||||
* 길찾기 = 연결선 한 구간씩(244-13 · `findSegmentKm` · 거래처 → 기준점(또는 시점) · 기준점 → 시점) ·
|
||||
* 길찾기 = 연결선 한 구간씩(244-13 · `findSegmentKm` · 거래처 · 기준점 → to(기준점 · 시점) · 254) ·
|
||||
* 자동 그림 = 거래처 쪽 구간 길 선 · 표지 둘(거래처 · 기준점 또는 시점)
|
||||
* 장소 그림(256-1) = 상자 좌표가 정해지면 `capturePlace` → 서버 캡처 `place/<상자 id>.png` ·
|
||||
* [저장] 뒤엔 서버가 바뀐 상자만 다시(다음 읽기에 보임) · `placeImageKey` · `placeImageBlobUrl`
|
||||
* 기준점 여럿(254) = `bases[]` · 길 선 표 키 = 거래처 · 기준점 id · 옛 꼴 캐시 = 읽을 때 새 꼴(`migrateMilestone`)
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { clearState, readState, writeState } from "../A00_Common/b_page_state";
|
||||
import { request } from "./B08_Quantity_UI_Hydro";
|
||||
import { PAVED, type MilestoneData, type SegKind } from "./B08_Quantity_Milestone_Calc";
|
||||
import { mapMarks, putAutoKm, segmentEnds } from "./B08_Quantity_Milestone_Layout";
|
||||
import {
|
||||
PAVED,
|
||||
migrateMilestone,
|
||||
type MilestoneData,
|
||||
type SegKind,
|
||||
} from "./B08_Quantity_Milestone_Calc";
|
||||
import { mapMarks, ownerOf, putAutoKm, segmentEnds } from "./B08_Quantity_Milestone_Layout";
|
||||
import type { SiteAddress } from "./B08_Quantity_UI_Milestone_Modal";
|
||||
|
||||
export interface SiteInfo {
|
||||
@@ -43,7 +49,7 @@ interface Payload {
|
||||
|
||||
export const EMPTY: MilestoneData = {
|
||||
site: { mid_station_m: null, mid_road: "비포장" },
|
||||
base: { enabled: false, name: "", address: "", dir: null, legs: [] },
|
||||
bases: [],
|
||||
routes: [],
|
||||
};
|
||||
|
||||
@@ -101,9 +107,11 @@ export const routeImageKey = (routeId: string): string | null => {
|
||||
export const hasMilestoneDraft = (projectId: string): boolean =>
|
||||
readState<MilestoneData>("b08-milestone", projectId) !== null;
|
||||
|
||||
/** 지금 값 — 캐시 → 저장본 → 빈 꼴(늘 복사본) */
|
||||
/** 지금 값 — 캐시 → 저장본 → 빈 꼴(늘 복사본 · 옛 꼴 캐시 = 새 꼴 · 254) */
|
||||
export function liveMilestone(projectId: string): MilestoneData {
|
||||
return clone(readState<MilestoneData>("b08-milestone", projectId) ?? saved?.data ?? EMPTY);
|
||||
return migrateMilestone(
|
||||
clone(readState<MilestoneData>("b08-milestone", projectId) ?? saved?.data ?? EMPTY),
|
||||
);
|
||||
}
|
||||
|
||||
/** 고친 한 벌을 캐시에 · 화면에 알림 */
|
||||
@@ -255,7 +263,7 @@ export function roadOptions(kind: SegKind): Array<{ value: string; text: string
|
||||
return [PAVED[kind], "비포장"].map((r) => ({ value: r, text: r }));
|
||||
}
|
||||
|
||||
/** 마지막 [길찾기] 길 선 — 구간(from = 거래처 id · "base") → [위도, 경도] 목록(메모리만 · 저장 안 함) */
|
||||
/** 마지막 [길찾기] 길 선 — 구간(from = 거래처 · 기준점 id) → [위도, 경도] 목록(메모리만 · 저장 안 함) */
|
||||
const paths = new Map<string, Array<[number, number]>>();
|
||||
export const segmentPath = (from: string): Array<[number, number]> => paths.get(from) ?? [];
|
||||
/** 경로 탭 · 자동 그림 길 선 = 거래처 쪽 구간 */
|
||||
@@ -270,7 +278,7 @@ export async function findSegmentKm(
|
||||
image = true,
|
||||
): Promise<string> {
|
||||
const data = liveMilestone(projectId);
|
||||
const owner = from === "base" ? data.base : data.routes.find((r) => r.id === from);
|
||||
const owner = ownerOf(data, from);
|
||||
if (owner?.manual) return ""; // 155 수기 입력 = 카카오 길찾기 안 함
|
||||
const seg = segmentEnds(data, from, siteInfo()?.start_lonlat ?? null);
|
||||
if (!seg) return "";
|
||||
@@ -294,9 +302,8 @@ export async function findSegmentKm(
|
||||
if ((res.path?.length ?? 0) >= 2) paths.set(from, res.path!);
|
||||
else paths.delete(from);
|
||||
const fresh = liveMilestone(projectId); // 기다리는 동안 고친 값 위에
|
||||
const legs = from === "base" ? fresh.base.legs : fresh.routes.find((r) => r.id === from)?.legs;
|
||||
if (legs?.[0]?.auto_km !== res.km) {
|
||||
putAutoKm(fresh, from, seg.key, res.km);
|
||||
if (ownerOf(fresh, from)?.legs[0]?.auto_km !== res.km) {
|
||||
putAutoKm(fresh, from, res.km);
|
||||
putMilestone(projectId, fresh);
|
||||
} else changes.dispatchEvent(new Event("change"));
|
||||
if (image && seg.key === "route" && paths.has(from))
|
||||
|
||||
Reference in New Issue
Block a user