Merge remote-tracking branch 'origin/sub_laptop_2' into sub_laptop_4

This commit is contained in:
2026-10-11 16:37:04 +09:00
6 changed files with 228 additions and 186 deletions
@@ -20,7 +20,6 @@ import { createButton, el, showToast } from "@ui/ui_template_elements";
import { attachHoverInfo } from "@ui/ui_template_hover_info";
import type { Row } from "../B09_Estimation/B09_Estimation_Calc_Base";
import { openHaulModal } from "../B09_Estimation/B09_Estimation_UI_Haul_Modal";
import { openPriceLink } from "../B09_Estimation/B09_Estimation_UI_Tabs_Bill_Link";
import { computeMilestone, type MilestoneRoute } from "./B08_Quantity_Milestone_Calc";
import { createDiagram } from "./B08_Quantity_UI_Milestone_Diagram";
import {
@@ -163,7 +162,6 @@ function tableView(projectId: string): MilestoneView {
dist: at && { L1: at.L1, L2: at.L2 },
name: route.rows[at?.index ?? 0]?.item || route.name || "운반",
apply: link,
other: () => void openPriceLink({ apply: link }),
}),
],
...(route.ref ? [["단가연결 풀기", () => link(null)] as [string, () => void]] : []),
+97 -166
View File
@@ -1,13 +1,13 @@
/* =============================================================================
* B09_Estimation_UI_Haul_Modal.ts
* 「운반 단가」 모달(PLAN 253-3 · 요구 162-1) — 운반공 줄 하나(거리이정표 행 하나)의 단가연결.
* 위 = 단가산출 이름 · [변경](`openLogicPicker`) · [다른 종류](옛 단가연결 창) ·
* 왼 = 옵션 값 — 로직 입력(`fetchLogicInputs`) + 사이클 입력(L1 · L2 · V1~V4 · t1 · t3~t6 · t1 셈 값) ·
* 오른 = 계산 절차 — 사이클 식 줄(TS 짝 `B08_Quantity_Haul_Cycle`) + 로직 산출 줄
* (서버 `POST …/estimation/logic-preview` · 값 바꾸면 다시 · 최신 응답만).
* 로직 입력 Cm · 사이클시간 · t2 · 운반거리 = 거리에서 채움(「거리에서」) · 손으로 고치면 덮음(`own`) ·
* 빈칸으로 지우면 다시 거리에서.
* 연결 없는 줄도 같은 모달(단가산출 빈칸 → [고르기]) · [적용] = 부르는 쪽 `apply`(캐시 → [저장] 때 서버 정본).
* 「운반 단가」 모달(PLAN 253-3 · 263-1 · 요구 162-1 · 164-1) — 운반공 줄 하나(거리이정표 행 하나)의 단가연결.
* 위 = 고른 항목 이름 · [변경](단가연결 한 목록 `openPriceLink` — 단가산출 · 사전조합 · 구조물 조합 · 호표) ·
* 왼 = 고른 항목이 요구하는 입력만(단가산출 = 로직 입력 `fetchLogicInputs` · 호표 = 입력 없음) ·
* 오른 = 계산 절차 — 거리 줄 · 사이클 식 줄(TS 짝 `B08_Quantity_Haul_Cycle` · 8-2-8 기본값 · 거리 칸 있을 때만) +
* 로직 산출 줄(서버 `POST …/estimation/logic-preview` · 값 바꾸면 다시 · 최신 응답만).
* 거리 칸(운반거리 · Cm · 사이클시간 · t2) = 거리에서 채움(「거리에서」) · 손으로 고치면 덮음(`own`) ·
* 빈칸으로 지우면 다시 거리에서. 옛 사이클 손 값(`cycle`) = 칸 없음 · [적용]하면 지움(기본값).
* 연결 없는 줄도 같은 모달(빈칸 → [고르기]) · [적용] = 부르는 쪽 `apply`(캐시 → [저장] 때 서버 정본).
* 여는 곳 = B08 거리이정표 표 우클릭 · B09 설계내역서 운반공 줄 단가연결.
* ========================================================================== */
@@ -22,65 +22,28 @@ import {
fillGiven,
type Dist,
} from "../B08_Quantity/B08_Quantity_Haul_Cycle";
import { loadTable, type TableRow } from "../M01_MasterData/M01_MasterData_UI_Logic_Note";
import type { ItemInput } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
import { fetchLogicInputs } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Api";
import { openLogicPicker } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Pick";
import { createValueControl } from "../M02_MasterTemplete/M02_Labor_Values";
import type { Row } from "./B09_Estimation_Calc_Base";
import {
importLib,
openPriceLink,
type Picked,
type PriceLinkOpts,
} from "./B09_Estimation_UI_Tabs_Bill_Link";
type Apply = (ref: Row | null, from?: Row) => void;
export interface HaulModalOpts {
export interface HaulModalOpts extends PriceLinkOpts {
/** 지금 연결(없으면 null) */
ref: Row | null;
/** 그 행 거리 — L1 포장 계 · L2 비포장(km) */
dist: Dist;
/** 줄 이름(제목) */
name: string;
apply: Apply;
/** 옛 단가연결 창(일위대가 · 구조물 조합 등 다른 종류) */
other?: () => void;
}
/** 사이클 입력 칸 — [이름, 글, 단위] · 무리 = 화면 묶음 */
const CYCLE_FIELDS: Array<[string, string, string][]> = [
[
["L1", "포장 운반거리 L1", "km"],
["L2", "비포장 운반거리 L2", "km"],
],
[
["V1", "포장 적재 V1", "km/hr"],
["V2", "포장 공차 V2", "km/hr"],
["V3", "비포장 적재 V3", "km/hr"],
["V4", "비포장 공차 V4", "km/hr"],
],
[
["t1", "적재 t1", "분"],
["T", "적재량 T", "ton"],
["rt", "단위중량 rt", "t/㎥"],
["f", "체적환산 f", ""],
["qs", "버킷용량 qs", "㎥"],
["k", "버킷계수 k", ""],
["Es", "작업효율 Es", ""],
["Cms", "적재기계 Cms", "초"],
],
[
["t3", "적하 t3", "분"],
["t4", "대기 t4", "분"],
["t5", "덮개 t5", "분"],
["t6", "세륜 t6", "분"],
],
];
/** 표 고르기(건설품셈 8-2-8 · master_data 읽기만) — [표 키, 채울 칸(값칸 → 입력), 조건 칸들] */
const PICKS: Array<[string, Record<string, string>, string]> = [
["CC000054", { 적재: "V1", 공차: "V2" }, "포장 도로상태"],
["CC000054", { 적재: "V3", 공차: "V4" }, "비포장 도로상태"],
["CC000055", { 시간: "t3" }, "적하(토질 · 작업조건)"],
["CC000056", { 시간: "t4" }, "대기(적재장소)"],
["CC000057", { 시간: "t5" }, "덮개"],
["CC000058", { 시간: "t6" }, "세륜"],
];
const OWNABLE = new Set(["L1", "L2", "t1"]);
/** 옛 사이클 손 칸(253-3) — 새 모달은 받지 않음 */
const OLD_OWN = new Set(["L1", "L2", "t1"]);
const FROM_DIST = "거리에서";
const fmt = (v: number | null | undefined): string =>
@@ -91,15 +54,11 @@ export function cycleLines(input: Record<string, unknown>): string[] {
const v: Record<string, number> = { ...CYCLE_DEFAULTS };
for (const k of Object.keys(v)) if (typeof input[k] === "number") v[k] = input[k] as number;
const r = cycleTime(input);
const t1 =
typeof input.t1 === "number"
? `t1 = ${fmt(r.t1)} 분(손 입력)`
: `t1 = Cms × n / (60 × Es) = ${v.Cms} × ${r.n} / (60 × ${v.Es}) = ${r.t1} 분`;
return [
"사이클시간 — 건설품셈 8-2-8 덤프트럭",
`q = T / rt × f = ${v.T} / ${v.rt} × ${v.f} = ${r.q} ㎥`,
`n = q / (qs × k) = ${r.q} / (${v.qs} × ${v.k}) = ${r.n} 회`,
t1,
`t1 = Cms × n / (60 × Es) = ${v.Cms} × ${r.n} / (60 × ${v.Es}) = ${r.t1} 분`,
`t2 = (L1/V1 + L1/V2 + L2/V3 + L2/V4) × 60 = (${v.L1}/${v.V1} + ${v.L1}/${v.V2} + ${v.L2}/${v.V3} + ${v.L2}/${v.V4}) × 60 = ${r.t2} 분`,
`Cm = t1 + t2 + t3 + t4 + t5 + t6 = ${r.t1} + ${r.t2} + ${v.t3} + ${v.t4} + ${v.t5} + ${v.t6} = ${r.cm} 분`,
`운반거리 = L1 + L2 = ${r.L} km`,
@@ -127,21 +86,33 @@ async function preview(code: string, given: Row, rate: unknown, picks: unknown):
}
const label = (text: string, unit: string): string => (unit ? `${text} (${unit})` : text);
const KIND_TEXT: Record<string, string> = {
logic: "단가산출",
precombo: "일위대가 사전조합",
combo: "구조물 조합",
dg: "일위대가 호표",
sg: "단가산출 호표",
};
/** 「운반 단가」 모달 */
export function openHaulModal(o: HaulModalOpts): void {
const logicRef = o.ref?.tab === "logic" ? o.ref : null;
const old = (logicRef?.cycle as Row) || {};
const st = {
/** 고른 것 — 단가산출 아닌 것(조합 · 호표) */
other: (o.ref && !logicRef
? { kind: o.ref.tab, ref: o.ref, from: { name: o.ref.name ?? o.ref.key } }
: null) as Picked | null,
code: String(logicRef?.code ?? ""),
name: String(logicRef?.name ?? ""),
given: { ...((logicRef?.given as Row) || {}) } as Row,
cycle: { ...((logicRef?.cycle as Row) || {}) } as Row,
own: new Set<string>((logicRef?.own as string[]) || []),
own: new Set<string>(((logicRef?.own as string[]) || []).filter((n) => !OLD_OWN.has(n))),
rate: logicRef?.rate ?? null,
picks: logicRef?.picks ?? null,
inputs: [] as ItemInput[],
unit: "",
};
const oldCycle = Object.keys(old).length > 0;
let seq = 0;
let timer = 0;
openModal({
@@ -159,65 +130,46 @@ export function openHaulModal(o: HaulModalOpts): void {
attrs: { style: "margin:0;white-space:pre-wrap;font-size:12px;line-height:1.6" },
});
const right = el("div", { attrs: { style: "overflow:auto;min-width:0" }, children: [steps] });
const ref = (): Row => ({ own: [...st.own], cycle: st.cycle, given: st.given });
// 사이클 = 8-2-8 기본값 + 거리(옛 손 값 안 씀 · [적용]하면 지움)
const ref = (): Row => ({ own: [...st.own], cycle: {}, given: st.given });
const names = (): string[] => st.inputs.map((i) => i.이름);
const filled = (): Row => fillGiven(ref(), names(), o.dist) as Row;
const redraw = (): void => {
const fills = st.code !== "" && names().some((n) => n in CYCLE_FILL);
const input = cycleInputs(ref(), o.dist);
const head2 = cycleLines(input);
steps.textContent = [...head2, "", st.code ? "단가산출 — 셈 중…" : "단가산출 — 없음"].join(
"\n",
);
const head2 = fills
? [
`거리 — 포장 ${fmt(o.dist?.L1)} km + 비포장 ${fmt(o.dist?.L2)} km`,
...cycleLines(input),
"",
]
: [];
window.clearTimeout(timer);
if (!st.code) return;
const my = ++seq;
if (!st.code) {
const what = st.other
? `${KIND_TEXT[st.other.kind] ?? ""} — 입력 없음 · 그대로 연결`
: "없음";
steps.textContent = `연결 — ${what}`;
return;
}
steps.textContent = [...head2, "단가산출 — 셈 중…"].join("\n");
timer = window.setTimeout(() => {
preview(st.code, filled(), st.rate, st.picks).then(
(got) => {
if (my !== seq) return; // 최신 응답만
st.unit = got.unit;
const price = got.price == null ? `빈칸 — ${got.why ?? ""}` : `${got.price} 원`;
steps.textContent = [...head2, "", ...got.lines, "", `단가 = ${price}`].join("\n");
steps.textContent = [...head2, ...got.lines, "", `단가 = ${price}`].join("\n");
},
(error: unknown) => {
if (my === seq)
steps.textContent = [...head2, "", `못 셈 — ${String(error)}`].join("\n");
if (my === seq) steps.textContent = [...head2, `못 셈 — ${String(error)}`].join("\n");
},
);
}, 250);
};
const numberField = (name: string, text: string, unit: string): HTMLElement => {
const input = el("input", {
className: "ui-input m02-inputval__control",
attrs: { type: "number", step: "any", "data-haul-cycle": name },
}) as HTMLInputElement;
const tag = el("span", { className: "m02-labin__muted" });
const show = (): void => {
const now = cycleInputs(ref(), o.dist);
const auto =
name === "t1" ? cycleTime(now).t1 : ((now[name] as number) ?? CYCLE_DEFAULTS[name]);
const mine = OWNABLE.has(name) ? st.own.has(name) : name in st.cycle;
if (document.activeElement !== input)
input.value = mine ? String(st.cycle[name] ?? "") : "";
input.placeholder = String(auto ?? "");
tag.textContent = (name === "L1" || name === "L2") && !mine && o.dist ? FROM_DIST : "";
};
input.addEventListener("change", () => {
const v = input.value.trim() === "" ? null : Number(input.value);
if (v === null || !Number.isFinite(v)) {
delete st.cycle[name];
st.own.delete(name);
} else {
st.cycle[name] = v;
if (OWNABLE.has(name)) st.own.add(name);
}
update();
});
shows.push(show);
return row(label(text, unit), [input, tag]);
};
const row = (text: string, cells: HTMLElement[]): HTMLElement =>
el("div", {
className: "m02-inputval__row",
@@ -228,33 +180,6 @@ export function openHaulModal(o: HaulModalOpts): void {
});
const shows: Array<() => void> = [];
const tablePick = (key: string, map: Record<string, string>, text: string): HTMLElement => {
const select = el("select", {
className: "ui-select ui-select--sm m02-inputval__control",
attrs: { "data-haul-pick": Object.values(map).join(",") },
}) as HTMLSelectElement;
select.append(el("option", { text: "표에서 고르기", attrs: { value: "" } }));
void loadTable(key).then((t: TableRow | null) => {
const conds = Object.keys(t?.조건 || {});
(t?.줄 || []).forEach((line, i) => {
const what = conds.map((c) => String(line[c] ?? "")).join(" · ");
const vals = Object.keys(map)
.map((col) => `${map[col]} ${String(line[col] ?? "")}`)
.join(" · ");
select.append(el("option", { text: `${what} — ${vals}`, attrs: { value: String(i) } }));
});
select.onchange = () => {
const line = t?.줄?.[Number(select.value)];
if (!line) return;
for (const [col, name] of Object.entries(map))
if (typeof line[col] === "number") st.cycle[name] = line[col] as number;
select.value = "";
update();
};
});
return row(text, [select]);
};
const logicRows = (): HTMLElement[] =>
st.inputs.map((input) => {
const fill = input.이름 in CYCLE_FILL && !st.own.has(input.이름);
@@ -291,29 +216,36 @@ export function openHaulModal(o: HaulModalOpts): void {
shows.forEach((show) => show());
redraw();
};
/** 로직 바뀜 — 왼쪽 칸을 새로 */
const muted = (text: string): HTMLElement => el("p", { className: "m02-labin__muted", text });
/** 고른 것 바뀜 — 왼쪽 칸을 새로 */
const refresh = (): void => {
shows.splice(0);
const sec = (text: string): HTMLElement =>
el("h4", { text, attrs: { style: "margin:12px 0 4px" } });
left.replaceChildren(
sec("단가산출 옵션 값"),
...(st.code
const lines = st.code
? st.inputs.length
? logicRows()
: [el("p", { className: "m02-labin__muted", text: "단가산출 없음 — [고르기]" })]),
sec("사이클시간 입력"),
...CYCLE_FIELDS.flatMap((group) => group.map(([n, t, u]) => numberField(n, t, u))),
sec("표에서 고르기(건설품셈 8-2-8)"),
...PICKS.map(([key, map, text]) => tablePick(key, map, text)),
: [muted("입력 없음")]
: [muted(st.other ? "입력 없음 — 그대로 연결" : "연결 없음 — [고르기]")];
left.replaceChildren(
el("h4", { text: "옵션 값", attrs: { style: "margin:12px 0 4px" } }),
...lines,
...(oldCycle && st.code
? [muted("옛 사이클 손 값 — 셈은 8-2-8 기본값 · [적용]하면 지움")]
: []),
);
title.textContent = st.code ? `단가산출 ${st.code} ${st.name}` : "단가산출 — 없음";
change.querySelector(".ui-btn__label")!.textContent = st.code ? "변경" : "고르기";
const p = st.other;
title.textContent = st.code
? `단가산출 ${st.code} ${st.name}`
: p
? `${KIND_TEXT[p.kind] ?? p.kind} ${String("from" in p ? (p.from.name ?? "") : "name" in p ? p.name : "")}`
: "연결 — 없음";
change.querySelector(".ui-btn__label")!.textContent = st.code || p ? "변경" : "고르기";
update();
};
const useLogic = (code: string, name: string): void => {
fetchLogicInputs([code]).then(
(got) => {
st.other = null;
st.code = code;
st.name = name;
st.inputs = got.로직[0]?.입력 ?? [];
@@ -324,41 +256,40 @@ export function openHaulModal(o: HaulModalOpts): void {
(error: unknown) => showToast(`로직 입력을 못 읽음 — ${String(error)}`, "error"),
);
};
const usePicked = (p: Picked): void => {
if (p.kind === "logic") return useLogic(p.code, p.name);
st.other = p;
st.code = "";
st.inputs = [];
refresh();
};
const change = createButton({
label: "변경",
variant: "ghost",
size: "sm",
onClick: () => openLogicPicker((logic) => useLogic(logic.키, logic.이름)),
onClick: () => void openPriceLink({ ...o, onPick: usePicked }),
});
linkBox.append(title, change);
if (o.other)
linkBox.append(
createButton({
label: "다른 종류",
variant: "ghost",
size: "sm",
onClick: () => {
close();
o.other?.();
},
}),
);
const ok = createButton({
label: "적용",
onClick: () => {
if (!st.code) return showToast("단가산출을 먼저 고르세요", "error");
const next: Row = {
tab: "logic",
code: st.code,
name: st.name,
given: filled(),
rate: st.rate,
picks: st.picks,
cycle: st.cycle,
own: [...st.own],
};
o.apply(next, { name: st.name, unit: st.unit.replace(/^원\//, "") });
showToast("적용함 — [저장]하면 서버가 거리로 다시 셈", "success");
const p = st.other;
if (!st.code && !p) return showToast("연결할 것을 먼저 고르세요", "error");
if (p && (p.kind === "combo" || p.kind === "precombo")) void importLib(o, p);
else if (p && "ref" in p) o.apply(p.ref, p.from);
else {
const next: Row = {
tab: "logic",
code: st.code,
name: st.name,
given: filled(),
rate: st.rate,
picks: st.picks,
own: [...st.own],
};
o.apply(next, { name: st.name, unit: st.unit.replace(/^원\//, "") });
showToast("적용함 — [저장]하면 서버가 거리로 다시 셈", "success");
}
close();
},
});
@@ -61,13 +61,26 @@ export function askInputs(apply: ApplyRef, code: string, name: string, old?: Row
);
}
/** 단가연결 목록에서 고른 것(263-1 · 「운반 단가」 모달이 받아 보임) */
export type LibPick = {
kind: "combo" | "precombo";
layer: string;
owner: string | null;
key: string;
};
export type Picked =
| { kind: "logic"; code: string; name: string }
| (LibPick & { name: string; unit: string })
| { kind: "dg" | "sg"; ref: Row; from: Row };
/** 서고 것 → 프로젝트 사본 · 새 대가 호표(204-2) — 서버가 들여옴 → 받은 원가 자료(대가 블록 · 산근)만
* 바꿔 끼우고(사용자 캐시 그대로) 줄 연결 `{tab: dg, key: 블록 id, lib: 사본 폴더}` */
async function importLib(
export async function importLib(
o: { apply: ApplyRef; onPayload?: (payload: Row) => void },
pick: { kind: "combo" | "precombo"; layer: string; owner: string | null; key: string },
lib: LibPick,
): Promise<void> {
const apply = o.apply;
const pick = { kind: lib.kind, layer: lib.layer, owner: lib.owner, key: lib.key };
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
if (!projectId) return;
showToast("가져오는 중…", "info");
@@ -98,7 +111,7 @@ async function importLib(
}
/** 항목 줄 단가연결 메뉴(251-6) — [단가연결 모달, 그 밖(로직 입력값 · 단가연결 풀기)] · ref = 지금 연결 ·
* haul = 운반공 줄(253-3 · 그 행 거리 · 이름) → 「운반 단가」 모달(옛 창은 [다른 종류]) */
* haul = 운반공 줄(253-3 · 그 행 거리 · 이름) → 「운반 단가」 모달([변경] = 같은 목록 · 263-1) */
export function linkItems(
ctx: TabContext,
ref: Row | null,
@@ -106,14 +119,14 @@ export function linkItems(
haul?: { dist: Dist; name: string },
): [Leaf[], Leaf[]] {
const more: Leaf[] = [];
const priceLink = (): void =>
void openPriceLink({
apply,
hopyo: { dg: ctx.sheets.daega || [], sg: ctx.sheets.sg_list || [] },
onPayload: (p) => Object.assign(ctx.payload, p),
});
const link: PriceLinkOpts = {
apply,
hopyo: { dg: ctx.sheets.daega || [], sg: ctx.sheets.sg_list || [] },
onPayload: (p) => Object.assign(ctx.payload, p),
};
const priceLink = (): void => void openPriceLink(link);
const haulModal = (): void =>
openHaulModal({ ref, dist: haul?.dist, name: haul?.name || "", apply, other: priceLink });
openHaulModal({ ...link, ref, dist: haul?.dist, name: haul?.name || "" });
if (ref?.tab === "logic" && !haul)
more.push(["로직 입력값 고치기", () => askInputs(apply, ref.code, ref.name || "", ref)]);
if (ref) more.push(["단가연결 풀기", () => apply(null)]);
@@ -129,9 +142,18 @@ export interface PriceLinkOpts {
hopyo?: { dg: Row[]; sg: Row[] };
/** 서고 사본 들여온 뒤 받은 원가 자료(B09 = 화면 자료에 끼움) */
onPayload?: (payload: Row) => void;
/** 고른 것 받기(263-1 「운반 단가」 모달) — 없으면 단가산출 = 옵션 값 창 · 조합 = 사본 들여옴 · 호표 = 연결 */
onPick?: (picked: Picked) => void;
}
export async function openPriceLink(o: PriceLinkOpts): Promise<void> {
const pick =
o.onPick ??
((p: Picked): void => {
if (p.kind === "logic") askInputs(o.apply, p.code, p.name);
else if ("ref" in p) o.apply(p.ref, p.from);
else void importLib(o, p);
});
const hop = (tab: "dg" | "sg") =>
((tab === "dg" ? o.hopyo?.dg : o.hopyo?.sg) ?? []).map((r) => ({
kind: tab,
@@ -140,7 +162,7 @@ export async function openPriceLink(o: PriceLinkOpts): Promise<void> {
name: String(r.name || ""),
sub: String(r.spec || ""),
unit: String(r.unit || ""),
onPick: () => o.apply({ tab, key: r.key }, r),
onPick: () => pick({ kind: tab, ref: { tab, key: r.key }, from: r }),
}));
// 구조물 조합 = 마스터 · 회사 · 개인 한 목록(B08 [선택] 나무와 같은 원천)
const combos = (await loadRows().catch(() => [])).map((r) => ({
@@ -151,14 +173,16 @@ export async function openPriceLink(o: PriceLinkOpts): Promise<void> {
sub: `${LAYER_TEXT[r.layer ?? "system"]} · ${r.key}`,
unit: String(r.단위 || ""),
onPick: () =>
void importLib(o, {
pick({
kind: "combo",
layer: r.layer ?? "system",
owner: r.owner ?? null,
key: r.key,
name: itemName(r),
unit: String(r.단위 || ""),
}),
}));
openLogicPicker((logic) => askInputs(o.apply, logic.키, logic.이름), {
openLogicPicker((logic) => pick({ kind: "logic", code: logic.키, name: logic.이름 }), {
layer: "personal",
owner: null,
title: "단가연결",
@@ -174,7 +198,7 @@ export async function openPriceLink(o: PriceLinkOpts): Promise<void> {
]
: []),
],
pick: (ref) =>
void importLib(o, { kind: "precombo", layer: ref.층, owner: ref.주인, key: ref.키 }),
pick: (ref, name) =>
pick({ kind: "precombo", layer: ref.층, owner: ref.주인, key: ref.키, name, unit: "" }),
});
}
+1 -1
View File
@@ -139,6 +139,6 @@ console.log(JSON.stringify([refText(null),
]
# 램프 = 항목 칸 · 우클릭 = [단가연결] · 연결 있으면 [단가연결 풀기] · 창 = B09 와 같은 것
assert 'key !== "item" ? null : routeAt(i)?.ref ? `${LAMP} is-linked` : LAMP' in src
assert "openPriceLink({ apply: link })" in src and '"단가연결 풀기"' in src
assert "openHaulModal({" in src and '"단가연결 풀기"' in src # 263-1 = 운반 단가 모달 하나
diagram = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Milestone_Diagram.ts").read_text("utf-8")
assert "Math.abs(h - lastH) > 1" in diagram # 틀 높이 바뀜 = 다시 그림(아래 잘림 없음)
+2 -2
View File
@@ -48,8 +48,8 @@ def test_내역_줄_거리():
def test_모달_여는_곳():
modal = _src("B09_Estimation/B09_Estimation_UI_Haul_Modal.ts")
for want in ("/estimation/logic-preview", "openLogicPicker", "fetchLogicInputs", "fillGiven",
"cycleInputs", "my !== seq", "CC000054", "거리에서", "[고르기]"): # fmt: skip
for want in ("/estimation/logic-preview", "openPriceLink", "fetchLogicInputs", "fillGiven",
"cycleInputs", "my !== seq", "거리에서", "[고르기]"): # fmt: skip
assert want in modal, want
milestone = _src("B08_Quantity/B08_Quantity_UI_Milestone.ts")
assert "openHaulModal({" in milestone and "L1: paved" in milestone
+89
View File
@@ -0,0 +1,89 @@
"""263-1 「운반 단가」 모달 = 고른 항목 입력만 + 거리 자동 — 사이클 손 칸 · 8-2-8 표 고르기 없음 ·
[변경] = 단가연결 한 목록 · 거리 칸(운반거리 · Cm · 사이클시간 · t2) = 거리에서 · 손 값 이김 ·
옛 `cycle` = 서버 읽기 그대로(모달 [적용]하면 지움)."""
import json
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from B08_Quantity.B08_Quantity_Haul_Cycle import FILL, cycle_time, fill_given # noqa: E402
from B09_Estimation.B09_Estimation_Sangeun import _input_names, hand_sources # noqa: E402
DIST = {"L1": 23.8, "L2": 10.5}
def _src(name: str) -> str:
return (ROOT / name).read_text(encoding="utf-8")
def test_왼쪽_칸_고른_로직_입력만():
modal = _src("B09_Estimation/B09_Estimation_UI_Haul_Modal.ts")
for gone in ("data-haul-cycle", "CC000054", "사이클시간 입력", "표에서 고르기", "다른 종류",
"numberField", "loadTable"): # fmt: skip
assert gone not in modal, gone
# 칸 = 로직 입력 하나에 한 줄(createValueControl) · 사이클 줄 = 거리 칸 있을 때만
assert "st.inputs.map((input) =>" in modal and "createValueControl(" in modal
assert "names().some((n) => n in CYCLE_FILL)" in modal
assert "포장 ${fmt(o.dist?.L1)} km + 비포장 ${fmt(o.dist?.L2)} km" in modal
# 거리 받는 마스터 로직 = 거리 칸 하나씩
for code, want in (("GF000284", "운반거리"), ("GF000134", "Cm"), ("GF000283", "사이클시간")):
names = _input_names(code)
assert [n for n in names if n in FILL] == [want], (code, names)
def test_변경_한_목록():
modal = _src("B09_Estimation/B09_Estimation_UI_Haul_Modal.ts")
assert "openPriceLink({ ...o, onPick: usePicked })" in modal and "importLib(o, p)" in modal
link = _src("B09_Estimation/B09_Estimation_UI_Tabs_Bill_Link.ts")
assert "o.onPick ??" in link and "openHaulModal({ ...link, ref" in link
milestone = _src("B08_Quantity/B08_Quantity_UI_Milestone.ts")
assert "other:" not in milestone and "openPriceLink" not in milestone
def test_거리_칸_자동_손_값_이김():
ref = {"given": {"재료": "토사"}, "cycle": {}, "own": []}
got = fill_given(ref, _input_names("GF000284"), DIST)
assert got["운반거리"] == 34.3 # L1 + L2
cm = cycle_time(DIST)["cm"]
assert fill_given(ref, ["Cm"], DIST)["Cm"] == cm
assert fill_given(ref, ["사이클시간", "t2"], DIST) == {
"재료": "토사", "사이클시간": cm, "t2": cycle_time(DIST)["t2"]} # fmt: skip
mine = {"given": {"운반거리": 5}, "own": ["운반거리"]}
assert fill_given(mine, ["운반거리"], DIST)["운반거리"] == 5
def test_ts_짝_사이클_비움():
"""모달 = cycle {} · 옛 own L1 · L2 · t1 뺌 → TS fillGiven = 파이썬과 같음."""
calc = ROOT / "B08_Quantity" / "B08_Quantity_Haul_Cycle.ts"
ref = {"given": {"qt": 15}, "cycle": {}, "own": []}
body = (
f"\nconsole.log(JSON.stringify(fillGiven({json.dumps(ref)}, ['Cm', '운반거리'],"
f" {json.dumps(DIST)})));"
)
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript", "-e", calc.read_text(encoding="utf-8") + body],
capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == fill_given(ref, ["Cm", "운반거리"], DIST)
modal = _src("B09_Estimation/B09_Estimation_UI_Haul_Modal.ts")
assert (
"cycle: {}, given: st.given" in modal and 'OLD_OWN = new Set(["L1", "L2", "t1"])' in modal
)
assert "cycle: st.cycle" not in modal # [적용] 새 연결에 cycle 없음
def test_옛_cycle_서버_읽기_그대로():
old = {"tab": "logic", "code": "GF000134", "given": {"Cm": 1}, "cycle": {"V1": 20}}
user = {"haul_refs": {"haul:r-u1": old}, "haul_dist": {"haul:r-u1": DIST}}
((_k, _u, src),) = hand_sources(user)
assert src["given"]["Cm"] == cycle_time({**DIST, "V1": 20})["cm"]
new = {**old, "cycle": None}
user["haul_refs"]["haul:r-u1"] = new
((_k, _u, src),) = hand_sources(user)
assert src["given"]["Cm"] == cycle_time(DIST)["cm"]