- 모달 = 담은 로직 이름별 입력(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
238 lines
11 KiB
TypeScript
238 lines
11 KiB
TypeScript
/* =============================================================================
|
|
* B09_Estimation_UI_Tabs_Bill_Link.ts
|
|
* B09 설계내역서 연결 고르기(166-2 · 184-3) — 있는 호표(일위대가목록표 dg · 단가산출근거목록표 sg) ·
|
|
* 서고(마스터 · 회사 · 개인) 구조물조합 · 일위대가 사전조합(204-2 — 서버가 프로젝트 사본으로 들여와 새
|
|
* 대가 호표 · 같은 것 = 있는 사본) · M01 로직(새 산근 = 서버 셈 · [저장] 뒤 단가) · 로직 입력값 고치기 ·
|
|
* 연결 풀기.
|
|
* 251 단가연결 = 한 모달(`openPriceLink` · 종류 고르기 + 검색) — B09 설계내역서 · B08 거리이정표 같이 씀 ·
|
|
* 단가산출 = 옵션 값 창(`askInputs` · 160)
|
|
*
|
|
* 줄에 적는 일 = 부르는 쪽 `apply(ref, from)`(`…_UI_Tabs_Bill_Edit` · 저장 꼴 모름)
|
|
* 로직 · 사전조합 고르기 · 입력값 = M02 창 재사용 · 구조물조합 고르기 = B08 [선택] 나무(173) 재사용 ·
|
|
* 층 읽기 권한 = 서버(`M02_Layer_Access`)
|
|
* ========================================================================== */
|
|
|
|
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
|
import { showToast } from "@ui/ui_template_elements";
|
|
import { loadRows } from "@ui/ui_template_item_picker";
|
|
import { itemName, type PickerLayer } from "@ui/ui_template_item_picker_data";
|
|
import type { ItemInput } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
|
|
import { fetchLogicInputs } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Api";
|
|
import { openLaborInput } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Input";
|
|
import { openLogicPicker } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Pick";
|
|
import type { Dist } from "../B08_Quantity/B08_Quantity_Haul_Cycle";
|
|
import type { Row } from "./B09_Estimation_Calc_Base";
|
|
import { openHaulModal } from "./B09_Estimation_UI_Haul_Modal";
|
|
import type { TabContext } from "./B09_Estimation_UI_Tabs";
|
|
|
|
/** 연결 적용 — ref = null 이면 풀기 · from = 고른 호표(명칭 · 규격 · 단위 채움) */
|
|
export type ApplyRef = (ref: Row | null, from?: Row) => void;
|
|
export type Leaf = [string, () => void];
|
|
const LAYER_TEXT: Record<PickerLayer, string> = {
|
|
system: "마스터",
|
|
company: "회사",
|
|
personal: "개인",
|
|
};
|
|
|
|
/** M01 로직 → 입력값 창 → 연결(새 산근 · 단가는 [저장] 뒤 서버 셈) */
|
|
export function askInputs(apply: ApplyRef, code: string, name: string, old?: Row): void {
|
|
fetchLogicInputs([code]).then(
|
|
(got) => {
|
|
const logic = got.로직[0] ?? null;
|
|
openLaborInput({
|
|
title: `${code} ${name}`.trim(),
|
|
logic,
|
|
values: old?.given || {},
|
|
memo: null,
|
|
rate: old?.rate ?? null,
|
|
readOnly: false,
|
|
okLabel: "연결",
|
|
notice: "새 산근 = 단가산출근거 뒤 번호 · 단가는 [저장] 뒤 서버가 셈",
|
|
done: (next) => {
|
|
const unit = String(logic?.결과단위 || "")
|
|
.replace(/^원\//, "")
|
|
.replace(/^[\d.]+/, "");
|
|
const ref = { tab: "logic", code, name, given: next.입력값, rate: next.켜쌓기율 };
|
|
apply({ ...ref, picks: old?.picks ?? null }, { name, unit });
|
|
showToast("연결함 — 새 산근 단가는 [저장] 뒤에 채워짐", "success");
|
|
},
|
|
});
|
|
},
|
|
(error: unknown) => showToast(`로직 입력을 못 읽음 — ${String(error)}`, "error"),
|
|
);
|
|
}
|
|
|
|
/** 단가연결 목록에서 고른 것(263-1 · 「운반 단가」 모달이 받아 보임) ·
|
|
* lib = 있는 사본 폴더 · haul = 운반 값(263-2 · 손 값 {줄 id: {입력: 값}} · 덮은 칸 · 그 행 거리) */
|
|
export type LibPick = {
|
|
kind: "combo" | "precombo";
|
|
layer: string;
|
|
owner: string | null;
|
|
key: string;
|
|
lib?: string;
|
|
haul?: { given: Row; own: string[]; dist: Dist };
|
|
};
|
|
/** 담은 로직 줄 입력 · 값(263-2 · `POST …/estimation/library-inputs`) */
|
|
export interface LibLine {
|
|
id: string;
|
|
로직: string;
|
|
이름: string;
|
|
입력: ItemInput[];
|
|
값: Row;
|
|
}
|
|
|
|
/** 사본 폴더 이름 → 서고 항목(`종류@층@주인@키~해시` · 짝 `Bill_Library.folder_name`) */
|
|
export function parseLib(lib: string): LibPick | null {
|
|
const [kind, layer, owner, key] = lib.split("~")[0].split("@");
|
|
if ((kind !== "combo" && kind !== "precombo") || !key) return null;
|
|
return { kind, layer, owner: owner === "-" ? null : owner.replace("-", "/"), key, lib };
|
|
}
|
|
|
|
async function postLib(path: string, body: Row): Promise<Row> {
|
|
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
|
if (!projectId) throw new Error("프로젝트 없음");
|
|
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/estimation/${path}`, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
const got = (await response.json().catch(() => null)) as Row | null;
|
|
if (!response.ok || !got) {
|
|
const detail = got?.detail;
|
|
throw new Error(typeof detail === "string" ? detail : `HTTP ${response.status}`);
|
|
}
|
|
return got;
|
|
}
|
|
|
|
/** 서고 항목 담은 로직 입력 · 값(있는 사본이면 그 덮기 전 값) */
|
|
export async function fetchLibInputs(lib: LibPick): Promise<LibLine[]> {
|
|
const { kind, layer, owner, key } = lib;
|
|
const got = await postLib("library-inputs", { kind, layer, owner, key, lib: lib.lib ?? null });
|
|
return (got.lines as LibLine[]) || [];
|
|
}
|
|
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: 사본 폴더}` */
|
|
export async function importLib(
|
|
o: { apply: ApplyRef; onPayload?: (payload: Row) => void },
|
|
lib: LibPick,
|
|
): Promise<void> {
|
|
const apply = o.apply;
|
|
const { kind, layer, owner, key } = lib;
|
|
const pick = { kind, layer, owner, key, ...(lib.lib && { lib: lib.lib }), ...lib.haul };
|
|
showToast("가져오는 중…", "info");
|
|
try {
|
|
const got = await postLib("library", pick);
|
|
if (!got.key) throw new Error("사본은 들여왔으나 호표를 못 만듦");
|
|
const { user: _user, ...rest } = got.payload as Row;
|
|
o.onPayload?.(rest);
|
|
const from = ((rest.sheets?.daega as Row[]) || []).find((r) => r.key === got.key);
|
|
const hand = lib.haul ? { given: lib.haul.given, own: lib.haul.own } : {};
|
|
apply({ tab: "dg", key: got.key, lib: got.폴더, ...hand }, from);
|
|
showToast(
|
|
got.만듦 ? "새 호표로 연결함 — [저장]해야 연결이 남음" : "가져온 호표로 연결함",
|
|
"success",
|
|
);
|
|
} catch (error) {
|
|
showToast(`못 가져옴 — ${error instanceof Error ? error.message : error}`, "error");
|
|
}
|
|
}
|
|
|
|
/** 항목 줄 단가연결 메뉴(251-6) — [단가연결 모달, 그 밖(로직 입력값 · 단가연결 풀기)] · ref = 지금 연결 ·
|
|
* haul = 운반공 줄(253-3 · 그 행 거리 · 이름) → 「운반 단가」 모달([변경] = 같은 목록 · 263-1) */
|
|
export function linkItems(
|
|
ctx: TabContext,
|
|
ref: Row | null,
|
|
apply: ApplyRef,
|
|
haul?: { dist: Dist; name: string },
|
|
): [Leaf[], Leaf[]] {
|
|
const more: Leaf[] = [];
|
|
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({ ...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)]);
|
|
return [[["단가연결", haul ? haulModal : priceLink]], more];
|
|
}
|
|
|
|
/** 단가연결 모달(251 · 159 · 160) — 한 목록에 단가산출 로직 · 일위대가 사전조합 · 구조물 조합(마스터 · 회사 ·
|
|
* 개인 층 단추) · 호표(B09 · 있을 때) · 종류 고르기 + 검색 + 구분 · 상세구분 거름 ·
|
|
* 단가산출 = 옵션 값 창(`askInputs` · 160) · 사전조합 · 구조물 조합 = 프로젝트 사본 들여옴 → `{tab: dg, key, lib}` */
|
|
export interface PriceLinkOpts {
|
|
apply: ApplyRef;
|
|
/** B09 호표 목록(일위대가 · 단가산출) — 없으면 그 종류 없음(B08) */
|
|
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,
|
|
key: String(r.key),
|
|
tag: tab === "dg" ? `대가${r.no}` : `산근 ${r.no}호표`,
|
|
name: String(r.name || ""),
|
|
sub: String(r.spec || ""),
|
|
unit: String(r.unit || ""),
|
|
onPick: () => pick({ kind: tab, ref: { tab, key: r.key }, from: r }),
|
|
}));
|
|
// 구조물 조합 = 마스터 · 회사 · 개인 한 목록(B08 [선택] 나무와 같은 원천)
|
|
const combos = (await loadRows().catch(() => [])).map((r) => ({
|
|
kind: "combo",
|
|
key: `${r.layer}:${r.owner ?? ""}:${r.key}`,
|
|
tag: "구조물 조합",
|
|
name: itemName(r),
|
|
sub: `${LAYER_TEXT[r.layer ?? "system"]} · ${r.key}`,
|
|
unit: String(r.단위 || ""),
|
|
onPick: () =>
|
|
pick({
|
|
kind: "combo",
|
|
layer: r.layer ?? "system",
|
|
owner: r.owner ?? null,
|
|
key: r.key,
|
|
name: itemName(r),
|
|
unit: String(r.단위 || ""),
|
|
}),
|
|
}));
|
|
openLogicPicker((logic) => pick({ kind: "logic", code: logic.키, name: logic.이름 }), {
|
|
layer: "personal",
|
|
owner: null,
|
|
title: "단가연결",
|
|
extra: [...combos, ...hop("dg"), ...hop("sg")],
|
|
kindFilter: [
|
|
{ value: "logic", text: "단가산출" },
|
|
{ value: "precombo", text: "일위대가 사전조합" },
|
|
{ value: "combo", text: "구조물 조합" },
|
|
...(o.hopyo
|
|
? [
|
|
{ value: "dg", text: "일위대가 호표" },
|
|
{ value: "sg", text: "단가산출 호표" },
|
|
]
|
|
: []),
|
|
],
|
|
pick: (ref, name) =>
|
|
pick({ kind: "precombo", layer: ref.층, owner: ref.주인, key: ref.키, name, unit: "" }),
|
|
});
|
|
}
|