Files
Aislo/B08_Quantity/B08_Quantity_Milestone_Calc.ts
T

268 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* B08_Quantity_Milestone_Calc.ts
* B08 거리이정표 표 합(PLAN 244-1) — 화면 없음 · 저장 안 함 · import 없음(시험이 node 로 돌림).
*
* ⚠ 짝 = `B08_Quantity_Milestone_Calc.py`(같은 식) — 한쪽을 고치면 다른 쪽도 ·
* 거울 시험 `resources/tester/test_244_1_b08_milestone.py`.
* 조작 중은 이 파일이 바로 셈 · [저장] · 읽기 때 서버가 파이썬 짝으로 다시 셈.
*
* 표 한 줄 = 경로(업체) 구간 + 기준점 사슬 구간들(254 · 거래처 `to` → 기준점 `to` → … → 시점 ·
* `baseChain` · 방문 집합으로 돌기 끊음 · 안 이은 기준점에서 멈춤) + (목적지 = 중점이면
* 시점 → 중점 = 중점 측점 km) · 도로 구분 셋마다 합 · 포장 = 2차 + 1차 · 합계 = 포장 + 비포장 ·
* 끝수 = 소수 셋째 자리(반올림)
* 구간 km = 사용자 값 · 없으면 [길찾기] 자동 값(244-7)
* 도로 구분(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 · 254) — 거래처 · 기준점 → 기준점(via) · 기준점 → 시점(base) · 거래처 → 시점(direct) ·
* 시점 → 중점(mid) */
export type SegKind = "via" | "base" | "direct" | "mid";
/** 포장 종류 = 선 자리로 정함(152) — → 기준점 = 2차 · 기준점 → 시점 = 1차 · 거래처 → 시점 = 포장 */
export const PAVED: Record<SegKind, Road> = {
via: "2차포장",
base: "1차포장",
direct: "포장",
mid: "포장",
};
/** 그 구간 도로 구분 — 사용자는 비포장만 고름 · 그 밖(빈 값 · 옛 값) = 자리의 포장 종류 */
export const roadOf = (road: Road | null | undefined, kind: SegKind): Road =>
road === "비포장" ? "비포장" : PAVED[kind];
/** 도로 구분을 선 자리 포장 종류로 바꾼 사본(152) — 다이어그램 선 글 · 그림용(저장 값은 그대로) */
export function withRoads(data: MilestoneData): MilestoneData {
const fix = (legs: Leg[], kind: SegKind): Leg[] =>
legs.map((leg) => ({ ...leg, road: roadOf(leg.road, kind) }));
return {
...data,
site: { ...data.site, mid_road: roadOf(data.site.mid_road, "mid") },
bases: data.bases.map((b) => ({ ...b, legs: fix(b.legs, baseKind(b)) })),
routes: data.routes.map((r) => ({ ...r, legs: fix(r.legs, routeKind(r)) })),
};
}
/** 이음 대상 = 시점(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";
const base = data.bases.find((b) => b.id === from);
if (base) return baseKind(base);
const route = data.routes.find((r) => r.id === from);
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;
export type Dir = (typeof DIRS)[number];
export type Dest = "start" | "mid";
export interface Leg {
/** null = 사용자가 안 고름(자동 판정 없음 · 244-7) */
road: Road | null;
/** 사용자 값 — 있으면 우선 */
km: number | null;
/** [길찾기] 자동 값(km 만 저장 · 경로 좌표 · 응답은 저장 안 함) */
auto_km?: number | null;
}
export interface MilestoneRow {
item: string;
dest: Dest;
note: string;
}
export interface MilestoneRoute {
id: string;
/** 업체 이름 = 하위 탭 이름 */
name: string;
address: string;
/** 잇는 곳(254) — "site"(시점으로 바로) · 기준점 id */
to: string;
dir: Dir | null;
lon?: number | null;
lat?: number | null;
/** 다이어그램에 끌어 놓은 자리(153 · 왼 위 · 그림 좌표) — 없으면 방위 자동 자리 */
pos?: [number, number] | null;
/** 선 이음(155) — false = 아직 안 이음(요소 추가 뒤 · 연결 점으로 이음 · 구간 km 안 셈) · 없음 = 이음(옛 자료) */
linked?: boolean;
/** 수기 입력(155) — 카카오 길찾기 안 함 · km 손 입력만 */
manual?: boolean;
/** 운반 묶음 키(251 · 총괄자재 · B09 운반 줄과 이음) — 없음 = 사용자 [요소 추가] 상자 */
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;
/** 길찾기용 시점 덮개(255-2 · 경로 탭 시점 끌기 · 노선은 그대로) — 없음 = 노선 첫 점 */
start_pin?: { lon: number; lat: number } | null;
};
bases: MilestoneBase[];
routes: MilestoneRoute[];
}
export interface MilestoneTableRow {
route: string;
index: number;
item: string;
company: string;
dest: Dest;
s2: number;
s1: number;
pv: number;
un: number;
paved: number;
unpaved: number;
total: number;
note: string;
/** 도로 구분 안 고른 구간이 있음 */
pick: boolean;
}
type Key = "s2" | "s1" | "pv" | "un";
const KEYS: Record<string, Key> = { "2차포장": "s2", "1차포장": "s1", 포장: "pv", 비포장: "un" };
/** 소수 셋째 자리 반올림(파이썬 짝 floor(x × 1000 + 0.5) 과 같음) */
export const r3 = (value: number): number => Math.floor(value * 1000 + 0.5) / 1000;
/** 구간 km — 사용자 값 우선 · 없으면 자동 값 */
export const legKm = (leg: Leg): number | null => leg.km ?? leg.auto_km ?? null;
/** 도로 구분마다 더함 — 구간 자리로 포장 종류(152 · 안 고른 구간 없음) */
function addLegs(sums: Record<Key, number>, legs: Leg[] | undefined, kind: SegKind): void {
for (const leg of legs ?? []) sums[KEYS[roadOf(leg.road, kind)]] += Number(legKm(leg) ?? 0);
}
/** 시점 → 중점 km — 사용자 측점(없으면 기본 = 노선 ÷ 2) ÷ 1000 */
export function midKm(data: MilestoneData, midDefaultM: number | null): number {
const station = data.site?.mid_station_m ?? midDefaultM;
return Number(station ?? 0) / 1000;
}
/** 표 줄 — 경로 차례 · 경로 안 줄 차례 */
export function computeMilestone(
raw: MilestoneData,
midDefaultM: number | null,
): MilestoneTableRow[] {
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 };
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({
route: route.id,
index,
item: row.item ?? "",
company: route.name ?? "",
dest: row.dest ?? "start",
s2: r3(sums.s2),
s1: r3(sums.s1),
pv: r3(sums.pv),
un: r3(sums.un),
paved: r3(paved),
unpaved: r3(sums.un),
total: r3(paved + sums.un),
note: row.note ?? "",
pick: false,
});
});
}
return out;
}