feat(B09): 263-2 사전조합 · 구조물 조합 운반 = 담은 로직 입력 + 거리 값 사본
- 모달 = 담은 로직 이름별 입력(library-inputs) · 거리 칸 거리에서 · 손 값(own) 이김
- 사본 폴더 = 종류@층@주인@키~값해시8 · 같은 값 = 같은 사본 · 다른 거리 = 따로 · 옛 이름 읽힘
- 셈 때 서버가 거리로 사본 다시 고름(resolve_hauls · fix_keys) · [저장] = 바뀐 연결 저장 값에도 · prune
- 연결 {tab dg, key, lib, given, own} 저장 꼴 · 원본 읽기 전 모양 검사
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JywHxQuWPk6UYXTebyrBoU
This commit is contained in:
@@ -2,11 +2,13 @@
|
||||
* B09_Estimation_UI_Haul_Modal.ts
|
||||
* 「운반 단가」 모달(PLAN 253-3 · 263-1 · 요구 162-1 · 164-1) — 운반공 줄 하나(거리이정표 행 하나)의 단가연결.
|
||||
* 위 = 고른 항목 이름 · [변경](단가연결 한 목록 `openPriceLink` — 단가산출 · 사전조합 · 구조물 조합 · 호표) ·
|
||||
* 왼 = 고른 항목이 요구하는 입력만(단가산출 = 로직 입력 `fetchLogicInputs` · 호표 = 입력 없음) ·
|
||||
* 왼 = 고른 항목이 요구하는 입력만(단가산출 = 로직 입력 `fetchLogicInputs` · 호표 = 입력 없음 ·
|
||||
* 사전조합 · 구조물 조합 = 담은 로직 이름별 입력 `fetchLibInputs` · 263-2) ·
|
||||
* 오른 = 계산 절차 — 거리 줄 · 사이클 식 줄(TS 짝 `B08_Quantity_Haul_Cycle` · 8-2-8 기본값 · 거리 칸 있을 때만) +
|
||||
* 로직 산출 줄(서버 `POST …/estimation/logic-preview` · 값 바꾸면 다시 · 최신 응답만).
|
||||
* 거리 칸(운반거리 · Cm · 사이클시간 · t2) = 거리에서 채움(「거리에서」) · 손으로 고치면 덮음(`own`) ·
|
||||
* 빈칸으로 지우면 다시 거리에서. 옛 사이클 손 값(`cycle`) = 칸 없음 · [적용]하면 지움(기본값).
|
||||
* 조합 [적용] = 거리 · 손 값 사본(서버 · 같은 값 = 같은 사본) · 연결 {tab dg, key, lib, given, own}.
|
||||
* 연결 없는 줄도 같은 모달(빈칸 → [고르기]) · [적용] = 부르는 쪽 `apply`(캐시 → [저장] 때 서버 정본).
|
||||
* 여는 곳 = B08 거리이정표 표 우클릭 · B09 설계내역서 운반공 줄 단가연결.
|
||||
* ========================================================================== */
|
||||
@@ -27,8 +29,12 @@ import { fetchLogicInputs } from "../M02_MasterTemplete/M02_MasterTemplete_Labor
|
||||
import { createValueControl } from "../M02_MasterTemplete/M02_Labor_Values";
|
||||
import type { Row } from "./B09_Estimation_Calc_Base";
|
||||
import {
|
||||
fetchLibInputs,
|
||||
importLib,
|
||||
openPriceLink,
|
||||
parseLib,
|
||||
type LibLine,
|
||||
type LibPick,
|
||||
type Picked,
|
||||
type PriceLinkOpts,
|
||||
} from "./B09_Estimation_UI_Tabs_Bill_Link";
|
||||
@@ -98,11 +104,18 @@ const KIND_TEXT: Record<string, string> = {
|
||||
export function openHaulModal(o: HaulModalOpts): void {
|
||||
const logicRef = o.ref?.tab === "logic" ? o.ref : null;
|
||||
const old = (logicRef?.cycle as Row) || {};
|
||||
const libRef = o.ref?.tab === "dg" && o.ref.lib ? parseLib(String(o.ref.lib)) : null;
|
||||
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,
|
||||
other: (libRef
|
||||
? { ...libRef, name: String(o.ref?.name ?? ""), unit: "" }
|
||||
: o.ref && !logicRef
|
||||
? { kind: o.ref.tab, ref: o.ref, from: { name: o.ref.name ?? o.ref.key } }
|
||||
: null) as Picked | null,
|
||||
/** 조합 담은 로직 줄(263-2) · 손 값 {줄 id: {입력: 값}} · 덮은 칸 `줄 id|입력` */
|
||||
lines: [] as LibLine[],
|
||||
libGiven: structuredClone((libRef && (o.ref?.given as Row)) || {}) as Record<string, Row>,
|
||||
libOwn: new Set<string>((libRef && (o.ref?.own as string[])) || []),
|
||||
code: String(logicRef?.code ?? ""),
|
||||
name: String(logicRef?.name ?? ""),
|
||||
given: { ...((logicRef?.given as Row) || {}) } as Row,
|
||||
@@ -135,8 +148,18 @@ export function openHaulModal(o: HaulModalOpts): void {
|
||||
const names = (): string[] => st.inputs.map((i) => i.이름);
|
||||
const filled = (): Row => fillGiven(ref(), names(), o.dist) as Row;
|
||||
|
||||
const isLib = (p: Picked | null): p is LibPick & { name: string; unit: string } =>
|
||||
p?.kind === "combo" || p?.kind === "precombo";
|
||||
/** 거리 칸 값(거리 없으면 undefined · 서버 짝 `Bill_Library.haul_values`) */
|
||||
const fromDist = (name: string): unknown =>
|
||||
o.dist ? fillGiven({ given: {}, own: [], cycle: {} }, [name], o.dist)[name] : undefined;
|
||||
|
||||
const redraw = (): void => {
|
||||
const fills = st.code !== "" && names().some((n) => n in CYCLE_FILL);
|
||||
const fills =
|
||||
(st.code !== "" && names().some((n) => n in CYCLE_FILL)) ||
|
||||
(isLib(st.other) &&
|
||||
!!o.dist &&
|
||||
st.lines.some((l) => l.입력.some((i) => i.이름 in CYCLE_FILL)));
|
||||
const input = cycleInputs(ref(), o.dist);
|
||||
const head2 = fills
|
||||
? [
|
||||
@@ -147,6 +170,14 @@ export function openHaulModal(o: HaulModalOpts): void {
|
||||
: [];
|
||||
window.clearTimeout(timer);
|
||||
const my = ++seq;
|
||||
if (isLib(st.other)) {
|
||||
steps.textContent = [
|
||||
...head2,
|
||||
`${KIND_TEXT[st.other.kind]} — [적용]하면 거리 · 손 값 사본(같은 값 = 같은 사본)`,
|
||||
"단가 = [저장] 뒤 서버 셈",
|
||||
].join("\n");
|
||||
return;
|
||||
}
|
||||
if (!st.code) {
|
||||
const what = st.other
|
||||
? `${KIND_TEXT[st.other.kind] ?? ""} — 입력 없음 · 그대로 연결`
|
||||
@@ -180,36 +211,73 @@ export function openHaulModal(o: HaulModalOpts): void {
|
||||
});
|
||||
const shows: Array<() => void> = [];
|
||||
|
||||
/** 입력 칸 하나 — auto = 거리에서 채울 값(아니면 undefined) · set(null = 비움) */
|
||||
const valueRow = (
|
||||
input: ItemInput,
|
||||
auto: () => unknown,
|
||||
hand: () => unknown,
|
||||
set: (value: unknown) => void,
|
||||
): HTMLElement => {
|
||||
const now = auto();
|
||||
const field = createValueControl(
|
||||
input,
|
||||
((now === undefined ? hand() : now) as string | number | null) ?? null,
|
||||
(next, bad) => {
|
||||
if (bad) return;
|
||||
const empty = next === null || next === "";
|
||||
const num = typeof next === "string" && Number.isFinite(Number(next));
|
||||
set(empty ? null : num ? Number(next) : next);
|
||||
update();
|
||||
},
|
||||
);
|
||||
field.control.setAttribute("data-labor-input", input.이름);
|
||||
const tag = el("span", { className: "m02-labin__muted" });
|
||||
shows.push(() => {
|
||||
const value = auto();
|
||||
tag.textContent = value === undefined ? "" : FROM_DIST;
|
||||
if (value !== undefined && document.activeElement !== field.control)
|
||||
field.control.value = String(value ?? "");
|
||||
});
|
||||
return row(label(input.이름, input.단위 || ""), [field.control, tag, field.error]);
|
||||
};
|
||||
const logicRows = (): HTMLElement[] =>
|
||||
st.inputs.map((input) => {
|
||||
const fill = input.이름 in CYCLE_FILL && !st.own.has(input.이름);
|
||||
const value = fill ? filled()[input.이름] : st.given[input.이름];
|
||||
const field = createValueControl(
|
||||
const name = input.이름;
|
||||
const fill = name in CYCLE_FILL;
|
||||
return valueRow(
|
||||
input,
|
||||
(value as string | number | null) ?? null,
|
||||
(next, bad) => {
|
||||
if (bad) return;
|
||||
if (next === null || next === "") {
|
||||
delete st.given[input.이름];
|
||||
st.own.delete(input.이름);
|
||||
} else {
|
||||
st.given[input.이름] =
|
||||
typeof next === "string" && Number.isFinite(Number(next)) ? Number(next) : next;
|
||||
if (input.이름 in CYCLE_FILL) st.own.add(input.이름);
|
||||
}
|
||||
update();
|
||||
() => (fill && !st.own.has(name) ? filled()[name] : undefined),
|
||||
() => st.given[name],
|
||||
(value) => {
|
||||
if (value === null) delete st.given[name];
|
||||
else st.given[name] = value;
|
||||
if (fill && value !== null) st.own.add(name);
|
||||
else st.own.delete(name);
|
||||
},
|
||||
);
|
||||
field.control.setAttribute("data-labor-input", input.이름);
|
||||
const tag = el("span", { className: "m02-labin__muted" });
|
||||
shows.push(() => {
|
||||
const auto = input.이름 in CYCLE_FILL && !st.own.has(input.이름);
|
||||
tag.textContent = auto ? FROM_DIST : "";
|
||||
if (auto && document.activeElement !== field.control)
|
||||
field.control.value = String(filled()[input.이름] ?? "");
|
||||
});
|
||||
return row(label(input.이름, input.단위 || ""), [field.control, tag, field.error]);
|
||||
});
|
||||
/** 조합 = 담은 로직 이름별 입력(263-2) */
|
||||
const libRows = (): HTMLElement[] =>
|
||||
st.lines.flatMap((line) => [
|
||||
el("h5", { text: `${line.이름} (${line.로직})`, attrs: { style: "margin:8px 0 2px" } }),
|
||||
...line.입력.map((input) => {
|
||||
const name = input.이름;
|
||||
const id = `${line.id}|${name}`;
|
||||
const fill = name in CYCLE_FILL;
|
||||
return valueRow(
|
||||
input,
|
||||
() => (fill && !st.libOwn.has(id) ? fromDist(name) : undefined),
|
||||
() => st.libGiven[line.id]?.[name] ?? line.값[name],
|
||||
(value) => {
|
||||
const mine = (st.libGiven[line.id] ??= {});
|
||||
if (value === null) delete mine[name];
|
||||
else mine[name] = value;
|
||||
if (fill && value !== null) st.libOwn.add(id);
|
||||
else st.libOwn.delete(id);
|
||||
},
|
||||
);
|
||||
}),
|
||||
]);
|
||||
|
||||
/** 값 바뀜 — 칸은 그대로 두고 표시 · 계산 절차만 */
|
||||
const update = (): void => {
|
||||
@@ -224,7 +292,11 @@ export function openHaulModal(o: HaulModalOpts): void {
|
||||
? st.inputs.length
|
||||
? logicRows()
|
||||
: [muted("입력 없음")]
|
||||
: [muted(st.other ? "입력 없음 — 그대로 연결" : "연결 없음 — [고르기]")];
|
||||
: isLib(st.other)
|
||||
? st.lines.length
|
||||
? libRows()
|
||||
: [muted("담은 로직 입력 없음")]
|
||||
: [muted(st.other ? "입력 없음 — 그대로 연결" : "연결 없음 — [고르기]")];
|
||||
left.replaceChildren(
|
||||
el("h4", { text: "옵션 값", attrs: { style: "margin:12px 0 4px" } }),
|
||||
...lines,
|
||||
@@ -256,11 +328,25 @@ export function openHaulModal(o: HaulModalOpts): void {
|
||||
(error: unknown) => showToast(`로직 입력을 못 읽음 — ${String(error)}`, "error"),
|
||||
);
|
||||
};
|
||||
/** 조합 담은 로직 입력 읽고 왼쪽 새로 */
|
||||
const useLib = (p: LibPick): void => {
|
||||
fetchLibInputs(p).then(
|
||||
(lines) => {
|
||||
st.lines = lines;
|
||||
refresh();
|
||||
},
|
||||
(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 = [];
|
||||
st.lines = [];
|
||||
st.libGiven = {};
|
||||
st.libOwn.clear();
|
||||
if (isLib(p)) useLib(p);
|
||||
refresh();
|
||||
};
|
||||
const change = createButton({
|
||||
@@ -275,8 +361,11 @@ export function openHaulModal(o: HaulModalOpts): void {
|
||||
onClick: () => {
|
||||
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);
|
||||
if (isLib(p)) {
|
||||
const haul = { given: st.libGiven, own: [...st.libOwn], dist: o.dist };
|
||||
const { kind, layer, owner, key, lib } = p;
|
||||
void importLib(o, { kind, layer, owner, key, lib, haul });
|
||||
} else if (p && "ref" in p) o.apply(p.ref, p.from);
|
||||
else {
|
||||
const next: Row = {
|
||||
tab: "logic",
|
||||
@@ -305,7 +394,10 @@ export function openHaulModal(o: HaulModalOpts): void {
|
||||
}),
|
||||
);
|
||||
if (st.code) useLogic(st.code, st.name);
|
||||
else refresh();
|
||||
else {
|
||||
if (libRef) useLib(libRef);
|
||||
refresh();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user