- 오른쪽 상세를 카드 넷(도면 · 면적산출 · 재료산출 · 선택 입력)으로 — M02 와 같은 카드 한 벌 - 카드 [수정] = 모달 안 편집 화면(CAD · 스프레드시트 · 선택 입력 값 넣기) · [뒤로] = 선택지 - 고침은 프로젝트 복사본만 · 안 가져온 항목은 「프로젝트로 가져와 고침」 · 마스터가 바뀜 표시 + [새로 가져오기] - 서버 길(sub6 · sub4)이 없으면 약속 모양의 가짜 응답(이음 자리 FAKE) - B05 구조물 폼이 projectId 를 모달에 넘김 · 모달 넓힘 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EauA2ATmqNRbeV48s9CcBr
230 lines
8.9 KiB
TypeScript
230 lines
8.9 KiB
TypeScript
/* =============================================================================
|
|
* ui_template_item_picker_edit.ts
|
|
* 항목 고르기 모달의 **편집 화면**(계획서 25-2d) — 카드 [수정] 이 모달 안에서 이 화면으로 바뀜.
|
|
* 도면 = M02 CAD 부품 · 면적산출 · 재료산출 = M02 스프레드시트 · 선택 입력 = 값 받기 카드.
|
|
* 고침은 **프로젝트 복사본**에만 씀(`ProjectItems.write`) — 마스터 · 회사 · 개인 원본은 안 바뀜.
|
|
* [뒤로] = 선택지(나무 + 카드)로 · 저장 안 한 고침이 있으면 버릴지 물음.
|
|
* 편집 부품은 이 화면을 열 때만 `import()`(B05 · B06 번들에 안 섞임).
|
|
* ========================================================================== */
|
|
|
|
import { createButton, el, showConfirmDialog, showToast } from "./ui_template_elements";
|
|
import type { SpreadsheetHandle, Workbook } from "../A00_Common/spreadsheet/spreadsheet";
|
|
import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links";
|
|
import type { DrawingTemplateHandle } from "../M02_MasterTemplete/M02_MasterTemplete_Drawing";
|
|
import type {
|
|
InputsCardHandle,
|
|
LinkDoc,
|
|
ValueDoc,
|
|
} from "../M02_MasterTemplete/M02_MasterTemplete_InputsCard";
|
|
import type { StructureDoc } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
|
|
import type { EditRequest } from "./ui_template_item_picker_detail";
|
|
import { itemName } from "./ui_template_item_picker_data";
|
|
import { fakeLink, loadInputs, PART_OF } from "./ui_template_item_picker_project";
|
|
|
|
export interface EditView {
|
|
root: HTMLElement;
|
|
/** 붙인 뒤 부름 — 편집 부품을 받아 세움 */
|
|
start: () => void;
|
|
isDirty: () => boolean;
|
|
/** 저장 안 한 고침을 버릴지 묻고, 괜찮으면 부품을 치움 — false 면 그대로 */
|
|
leave: () => Promise<boolean>;
|
|
destroy: () => void;
|
|
}
|
|
|
|
const TEXT = {
|
|
back: "← 뒤로",
|
|
save: "저장",
|
|
saved: "프로젝트 복사본에 저장했음",
|
|
saveFailed: "저장하지 못함 — {value}",
|
|
loadFailed: "복사본을 못 읽음 — {value}",
|
|
discard: "저장 안 한 고침이 있음 — 버리고 돌아갈까요?",
|
|
outOfRange: "범위 밖 값이 있어 저장하지 못함",
|
|
noAreaPill: "면적산출 알약 없음 — 선택 입력에서 자리를 면적산출로 이으면 여기 뜸",
|
|
drawing: "도면",
|
|
area: "면적산출",
|
|
material: "재료산출",
|
|
inputs: "선택 입력",
|
|
suffix: "{value} 수정",
|
|
};
|
|
|
|
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
|
const fill = (text: string, value: string): string => text.replace("{value}", value);
|
|
|
|
export function createEditView(req: EditRequest, onBack: () => void): EditView {
|
|
const { kind, row, info, items } = req;
|
|
let dirty = false;
|
|
let saving = false;
|
|
let doc: unknown = null;
|
|
let drawing: DrawingTemplateHandle | null = null;
|
|
let sheet: SpreadsheetHandle | null = null;
|
|
let card: InputsCardHandle | null = null;
|
|
|
|
const word = {
|
|
structure: TEXT.drawing,
|
|
area: TEXT.area,
|
|
material: TEXT.material,
|
|
inputs: TEXT.inputs,
|
|
}[kind];
|
|
const back = createButton({ label: TEXT.back, variant: "ghost", size: "sm" });
|
|
const save = createButton({ label: TEXT.save, variant: "filled", size: "sm" });
|
|
save.disabled = true;
|
|
const note = el("span", { className: "ui-item-picker__edit-note" });
|
|
const body = el("div", { className: "ui-item-picker__edit-body" });
|
|
const root = el("div", {
|
|
className: "ui-item-picker__edit",
|
|
children: [
|
|
el("div", {
|
|
className: "ui-item-picker__edit-top",
|
|
children: [
|
|
back,
|
|
el("h4", {
|
|
className: "ui-item-picker__edit-title",
|
|
text: `${itemName(row)} — ${fill(TEXT.suffix, word)}`,
|
|
}),
|
|
note,
|
|
save,
|
|
],
|
|
}),
|
|
body,
|
|
],
|
|
});
|
|
|
|
const setDirty = (next: boolean): void => {
|
|
dirty = next;
|
|
save.disabled = !next || saving;
|
|
};
|
|
const fail = (text: string): void => {
|
|
body.replaceChildren(el("p", { className: "ui-item-picker__hint", text }));
|
|
};
|
|
|
|
/** 면적산출 알약 — 항목 연결에서 자리가 「면적」 인 입력(25-1b) */
|
|
async function areaLinks(): Promise<{ links: LinkOptions; notice?: string }> {
|
|
const { list, fake } = await loadInputs(row);
|
|
const link =
|
|
((await items.read(info.폴더, "입력연결")) as LinkDoc | null) ??
|
|
(fake ? fakeLink(list) : null);
|
|
const 항목 = (link?.줄 ?? [])
|
|
.filter((one) => one.자리 === "면적")
|
|
.map((one) => ({
|
|
키: one.알약 ?? `입력|${one.로직}|${one.입력}`,
|
|
이름: one.입력,
|
|
단위:
|
|
list.로직.find((logic) => logic.로직 === one.로직)?.입력.find((x) => x.이름 === one.입력)
|
|
?.단위 ?? "",
|
|
}));
|
|
return 항목.length
|
|
? { links: { 항목, 모드: "입력" } }
|
|
: { links: { 항목: [], 모드: "입력" }, notice: TEXT.noAreaPill };
|
|
}
|
|
|
|
async function mountSheet(kindOf: "area" | "material"): Promise<void> {
|
|
const [{ createSpreadsheet }, pills, { searchMaterials }] = await Promise.all([
|
|
import("../A00_Common/spreadsheet/spreadsheet"),
|
|
kindOf === "area"
|
|
? areaLinks()
|
|
: Promise.resolve({ links: undefined as LinkOptions | undefined, notice: undefined }),
|
|
import("../M01_MasterData/M01_MasterData_Api_Fetch"),
|
|
]);
|
|
const links: LinkOptions = pills.links ?? { 항목: [], 모드: "자재", 자재찾기: searchMaterials };
|
|
if (pills.notice) note.textContent = pills.notice;
|
|
const host = el("div", { className: "ui-item-picker__edit-sheet" });
|
|
body.replaceChildren(host);
|
|
sheet = createSpreadsheet(host, doc as Workbook, {
|
|
readOnly: false,
|
|
tabs: false,
|
|
onChange: () => setDirty(true),
|
|
links,
|
|
});
|
|
}
|
|
|
|
async function mountDrawing(): Promise<void> {
|
|
const { mountDrawingTemplate } =
|
|
await import("../M02_MasterTemplete/M02_MasterTemplete_Drawing");
|
|
const host = el("div", { className: "ui-item-picker__edit-cad" });
|
|
body.replaceChildren(host);
|
|
drawing = mountDrawingTemplate(host, (doc as StructureDoc).도면 as never, {
|
|
onSave: () => void doSave(),
|
|
readOnly: false,
|
|
name: `item:${info.폴더}`,
|
|
layer: "project",
|
|
projectId: req.projectId,
|
|
onChanged: (changed) => setDirty(changed),
|
|
originMarker: true,
|
|
noArea: true,
|
|
});
|
|
}
|
|
|
|
async function mountInputs(): Promise<void> {
|
|
const [{ createInputsCard }, loaded] = await Promise.all([
|
|
import("../M02_MasterTemplete/M02_MasterTemplete_InputsCard"),
|
|
loadInputs(row),
|
|
]);
|
|
const link =
|
|
((await items.read(info.폴더, "입력연결")) as LinkDoc | null) ??
|
|
(loaded.fake ? fakeLink(loaded.list) : null);
|
|
const values = ((await items.read(info.폴더, "입력값")) as ValueDoc | null) ?? {};
|
|
card = createInputsCard({ mode: "value", onChange: () => setDirty(true) });
|
|
card.set({ list: loaded.list, link, values });
|
|
body.replaceChildren(
|
|
el("div", { className: "ui-item-picker__edit-inputs", children: [card.root] }),
|
|
);
|
|
}
|
|
|
|
async function start(): Promise<void> {
|
|
try {
|
|
if (kind !== "inputs") {
|
|
doc = await items.read(info.폴더, PART_OF[kind]);
|
|
if (doc == null) return fail(fill(TEXT.loadFailed, "복사본에 없음"));
|
|
}
|
|
if (kind === "structure") await mountDrawing();
|
|
else if (kind === "inputs") await mountInputs();
|
|
else await mountSheet(kind);
|
|
} catch (error) {
|
|
fail(fill(TEXT.loadFailed, why(error)));
|
|
}
|
|
}
|
|
|
|
async function doSave(): Promise<void> {
|
|
if (saving) return;
|
|
saving = true;
|
|
save.disabled = true;
|
|
try {
|
|
if (kind === "inputs") {
|
|
if (!card?.isValid()) return void showToast(TEXT.outOfRange, "error");
|
|
await items.write(info.폴더, "입력값", card.getValues());
|
|
} else if (kind === "structure") {
|
|
const edited = await drawing?.getDoc();
|
|
await items.write(info.폴더, "도면", { ...(doc as StructureDoc), 도면: edited });
|
|
} else if (sheet) {
|
|
// 번호 칸은 스프레드시트가 모름 — 읽은 문서 위에 편집본을 얹음(M02 저장과 같은 규칙)
|
|
const merged = { ...(doc as Record<string, unknown>), ...sheet.getDoc() };
|
|
await items.write(info.폴더, PART_OF[kind], merged);
|
|
doc = merged;
|
|
}
|
|
setDirty(false);
|
|
showToast(TEXT.saved, "success");
|
|
} catch (error) {
|
|
showToast(fill(TEXT.saveFailed, why(error)), "error");
|
|
} finally {
|
|
saving = false;
|
|
save.disabled = !dirty;
|
|
}
|
|
}
|
|
|
|
const destroy = (): void => {
|
|
drawing?.destroy();
|
|
sheet?.destroy();
|
|
drawing = null;
|
|
sheet = null;
|
|
root.remove();
|
|
};
|
|
const leave = async (): Promise<boolean> => !dirty || showConfirmDialog(TEXT.discard);
|
|
|
|
save.addEventListener("click", () => void doSave());
|
|
back.addEventListener("click", async () => {
|
|
if (await leave()) onBack();
|
|
});
|
|
|
|
return { root, start: () => void start(), isDirty: () => dirty, leave, destroy };
|
|
}
|