feat(B09 설계내역서 펼침): 우클릭 복사해 새 일위대가 · 내 자료집에 저장 · 어디 쓰였나 (PLAN 703-2)
- 복사 = 덮개 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>
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
/* =============================================================================
|
||||
* 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]
|
||||
: []),
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user