- 복사 = 덮개 K:… 의 @copy {from · name · spec} + 원본 덮개째(B09 사용자 값 · 브레인 정함) — 셈 짝 Calc_Labor py·ts 가 원본 블록을 본뜬 새 호표(번호 끝 · 기본값 = 원본) · 이 줄 단가연결을 새 호표로 바꿀지 물음 · 복사본 지우기(잇는 줄 있으면 막음)
- 원본 없음 = 짝 잃음 알림 · 저장 다듬기 @copy 글만 · 서고 사본 원본 = prune 에서 지킴
- 내 자료집 = POST …/estimation/daega/personal → 개인 층 사전조합(B09_Estimation_Daega_Personal) — 넣을 값 · 고른 자재 · 줄 수량 덮개(손) 담음 · 품 · 단가 덮개 = 안 담은 수 알림 · 같은 이름 409 → 덮어쓰기 · 새 이름
- 어디 쓰였나 = 누른 줄 노임 · 자재 · 중기(그 밖 = 호표)를 쓰는 내역 줄 목록 → 누르면 그 줄로 · 펼침
- 새 파일 B09_Estimation_UI_Tabs_Bill_Fold_Menu.ts · 시험 test_703_b09_daega_copy(거울 · 임시 storage 길)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
209 lines
8.8 KiB
TypeScript
209 lines
8.8 KiB
TypeScript
/* =============================================================================
|
|
* B09_Estimation_UI_Tabs_Bill_Fold_Menu.ts
|
|
* B09 설계내역서 펼친 일위대가 우클릭(PLAN 703-2) — 복사해 새 일위대가 · 내 자료집에 저장 · 어디 쓰였나.
|
|
*
|
|
* 복사 = 이름 · 규격 묻기 → 덮개 `K:…` 에 `@copy` {from · name · spec} + 원본 덮개째(캐시 · 셈 짝이 새 호표 ·
|
|
* 번호 끝) → 「이 줄 단가연결을 새 호표로」 물음 · 복사본 지우기 = 그 덮개 통째(쓰는 줄 있으면 안 지움)
|
|
* 내 자료집 = POST …/estimation/daega/personal(화면 캐시 덮개째 · 서버가 개인 층 사전조합) ·
|
|
* 같은 이름 409 → 「덮어쓰기 · 새 이름」 · 담음 · 안 담음(품 · 단가 덮개) 수 알림
|
|
* 어디 쓰였나 = 누른 줄 노임 · 자재 · 중기(그 밖 = 그 호표)를 쓰는 내역 줄 목록 → 누르면 그 줄로
|
|
* ========================================================================== */
|
|
|
|
import { API_BASE_URL } from "@config/config_frontend";
|
|
import { showToast } from "@ui/ui_template_elements";
|
|
import { askFields } from "@ui/ui_template_item_picker_ask";
|
|
import { createListRow } from "@ui/ui_template_list_row";
|
|
import type { MapContextMenuItem } from "@ui/ui_template_context_menu";
|
|
import { askConfirm, askText, openModal } from "@ui/ui_template_modal";
|
|
import type { DataRow } from "@ui/ui_template_data_table";
|
|
import { createText } from "@ui/ui_template_text";
|
|
import { currentProjectId } from "../A00_Common/b_project_switch";
|
|
import type { Row } from "./B09_Estimation_Calc_Base";
|
|
import { COPY } from "./B09_Estimation_Calc_Labor";
|
|
import type { TabContext } from "./B09_Estimation_UI_Tabs";
|
|
import { editCell } from "./B09_Estimation_UI_Tabs_Bill_Edit";
|
|
|
|
/** 어디 쓰였나 — 같은 품목으로 찾는 줄 */
|
|
const GOODS = ["labor", "material", "machine"];
|
|
/** 부른 대가 따라가는 깊이 */
|
|
const DEPTH = 6;
|
|
|
|
const covers = (ctx: TabContext): Row => JSON.parse(JSON.stringify(ctx.user.daega_cover || {}));
|
|
const newKey = (): string => `K:${Math.random().toString(16).slice(2, 10).padEnd(8, "0")}`;
|
|
|
|
/** 복사해 새 일위대가 — 덮개째 · 번호 끝 · 이 줄 연결 바꿀지 물음 */
|
|
async function copyBlock(ctx: TabContext, raw: DataRow[], row: Row, block: Row): Promise<void> {
|
|
const got = await askFields({
|
|
title: "복사해 새 일위대가",
|
|
note: `「${block.name}」 을 이 공사 안 새 호표로 — 고친 칸까지 복사`,
|
|
fields: [
|
|
{ key: "name", label: "이름", value: `${block.name} 복사`, required: true },
|
|
{ key: "spec", label: "규격", value: block.spec || "" },
|
|
],
|
|
okLabel: "만들기",
|
|
closeLabel: "닫기",
|
|
emptyText: "{label} 이 빔",
|
|
});
|
|
if (!got) return;
|
|
const key = newKey();
|
|
const all = covers(ctx);
|
|
all[key] = { ...(all[block.cover] || {}), [COPY]: { from: block.cover, ...got } }; // 덮개째
|
|
const relink = await askConfirm({
|
|
title: "단가연결",
|
|
message: `「${row.name || ""}」 줄의 단가연결을 새 호표 「${got.name}」 로 바꿀지`,
|
|
okLabel: "바꿈",
|
|
cancelLabel: "그대로",
|
|
});
|
|
const next = { ...ctx, user: { ...ctx.user, daega_cover: all } };
|
|
const i = raw.findIndex((r) => r.key === row.key);
|
|
if (relink && i >= 0) editCell(next, raw, i, "ref", { tab: "dg", key: `DG:${key}` });
|
|
else ctx.setUser(next.user);
|
|
showToast(`새 호표 「${got.name}」 — 일위대가목록표 끝 · [저장] 때 서버가 다시 셈`, "success");
|
|
}
|
|
|
|
/** 복사본 지우기 — 그 호표를 잇는 내역 줄이 있으면 막음 */
|
|
function removeCopy(ctx: TabContext, raw: DataRow[], block: Row): void {
|
|
const used = raw.filter((r) => (r.ref as Row | undefined)?.key === block.id);
|
|
if (used.length) {
|
|
showToast(`잇는 내역 줄 ${used.length} — 단가연결을 먼저 바꿈`, "error");
|
|
return;
|
|
}
|
|
const all = covers(ctx);
|
|
delete all[block.cover];
|
|
ctx.setUser({ ...ctx.user, daega_cover: all });
|
|
}
|
|
|
|
async function postPersonal(body: Row): Promise<{ ok: boolean; status: number; got: Row }> {
|
|
const response = await fetch(
|
|
`${API_BASE_URL}/projects/${currentProjectId()}/estimation/daega/personal`,
|
|
{
|
|
method: "POST",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
},
|
|
);
|
|
const got = ((await response.json().catch(() => null)) as Row | null) ?? {};
|
|
return { ok: response.ok, status: response.status, got };
|
|
}
|
|
|
|
/** 내 자료집에 저장 — 개인 층 사전조합(덮개 · 넣을 값 · 고른 자재) · 같은 이름 물음 */
|
|
async function savePersonal(ctx: TabContext, block: Row): Promise<void> {
|
|
let name = await askText({
|
|
title: "내 자료집에 저장",
|
|
message: "개인 층 일위대가 사전조합으로 — 넣을 값 · 고른 자재 · 줄 수량 고친 칸까지",
|
|
value: block.name || "",
|
|
okLabel: "저장",
|
|
});
|
|
let overwrite: string | null = null;
|
|
while (name !== null && name.trim()) {
|
|
const body = { cover: block.cover, name: name.trim(), overwrite, user: ctx.user };
|
|
const { ok, status, got } = await postPersonal(body);
|
|
const same = got.detail?.같은이름;
|
|
if (ok) {
|
|
const skip: Row = got.안담음 || {};
|
|
const left = (skip.품 || 0) + (skip.단가 || 0);
|
|
const tail = left
|
|
? ` · 안 담은 고친 칸 ${left}(품 ${skip.품} · 단가 ${skip.단가}) — 품은 고정 줄(PLAN 705) 뒤 담김`
|
|
: "";
|
|
showToast(`내 자료집에 저장 「${got.이름}」(${got.키})${tail}`, "success", 6000);
|
|
return;
|
|
}
|
|
if (status !== 409 || !same) {
|
|
const detail = got.detail;
|
|
showToast(`저장 못 함 — ${typeof detail === "string" ? detail : status}`, "error");
|
|
return;
|
|
}
|
|
const over = await askConfirm({
|
|
title: "같은 이름",
|
|
message: `내 자료집에 「${name.trim()}」(${same}) 이 있음 — 덮어쓰기 · 새 이름`,
|
|
okLabel: "덮어쓰기",
|
|
cancelLabel: "새 이름",
|
|
});
|
|
if (over) overwrite = same;
|
|
else {
|
|
overwrite = null;
|
|
name = await askText({ title: "새 이름", message: "내 자료집 이름", value: `${name} 2` });
|
|
}
|
|
}
|
|
}
|
|
|
|
/** 블록 나무에 그 품목(종류 · 키) · 그 호표(id)가 있나 */
|
|
function holds(blocks: Map<string, Row>, bid: string, hit: (ln: Row) => boolean): boolean {
|
|
const seen = new Set<string>();
|
|
const walk = (id: string, depth: number): boolean => {
|
|
const b = blocks.get(id);
|
|
if (!b || seen.has(id) || depth > DEPTH) return false;
|
|
seen.add(id);
|
|
return (b.lines || []).some(
|
|
(ln: Row) => hit(ln) || (ln.kind === "daega" && walk(String(ln.key), depth + 1)),
|
|
);
|
|
};
|
|
return walk(bid, 0);
|
|
}
|
|
|
|
/** 어디 쓰였나 — 그 품목 · 호표를 쓰는 내역 줄 → 누르면 그 줄로 */
|
|
function whereUsed(
|
|
ctx: TabContext,
|
|
raw: DataRow[],
|
|
block: Row,
|
|
line: Row,
|
|
go: (key: string) => void,
|
|
): void {
|
|
const blocks = new Map<string, Row>(
|
|
(ctx.sheets.daega_blocks || []).map((b: Row) => [String(b.id), b]),
|
|
);
|
|
const goods = GOODS.includes(line.kind);
|
|
const id = line.kind === "daega" ? String(line.key) : String(block.id);
|
|
const label = goods ? `${line.name || line.key}` : `${blocks.get(id)?.name || ""}`;
|
|
const hit = goods
|
|
? (ln: Row) => ln.kind === line.kind && ln.key === line.key
|
|
: (ln: Row) => ln.kind === "daega" && ln.key === id;
|
|
const used = raw.filter((r) => {
|
|
const ref = r.ref as Row | undefined;
|
|
if (ref?.tab !== "dg") return false;
|
|
return (!goods && String(ref.key) === id) || holds(blocks, String(ref.key), hit);
|
|
});
|
|
openModal({
|
|
title: `어디 쓰였나 — 「${label}」`,
|
|
closeLabel: "닫기",
|
|
width: "40rem",
|
|
mount: (body, close) => {
|
|
body.append(
|
|
createText(`쓰는 내역 줄 ${used.length} · 누르면 그 줄로`, "muted"),
|
|
...used.map((r) =>
|
|
createListRow({
|
|
label: String(r.name || ""),
|
|
sub: [r.spec, r.qty != null ? `${r.qty} ${r.unit || ""}` : ""]
|
|
.filter(Boolean)
|
|
.join(" · "),
|
|
onClick: () => {
|
|
close();
|
|
go(String(r.key));
|
|
},
|
|
}),
|
|
),
|
|
);
|
|
},
|
|
});
|
|
}
|
|
|
|
/** 펼친 줄 우클릭 덧메뉴 — 복사 · 내 자료집 · 어디 쓰였나(+ 복사본 지우기) */
|
|
export function foldMenu(
|
|
ctx: TabContext,
|
|
raw: DataRow[],
|
|
row: Row,
|
|
block: Row,
|
|
line: Row | undefined,
|
|
go: (key: string) => void,
|
|
): MapContextMenuItem[] {
|
|
return [
|
|
["복사해 새 일위대가", () => void copyBlock(ctx, raw, row, block)],
|
|
["내 자료집에 저장", () => void savePersonal(ctx, block)],
|
|
["어디 쓰였나", () => whereUsed(ctx, raw, block, line || {}, go)],
|
|
...(String(block.cover).startsWith("K:")
|
|
? [["이 복사본 지우기", () => removeCopy(ctx, raw, block)] as MapContextMenuItem]
|
|
: []),
|
|
];
|
|
}
|