Files
Aislo/ui_template/ui_template_item_picker_edit.ts
T
eomsangdonandClaude Opus 5.5 c704f18982 refactor(M02): 선택 입력 지움(39-7b G3) — 카드 · 자리 고르기 · inputs/ 연결 · 입력 목록 · 연결 · 알약 길 · 프로젝트 복사본 「입력연결」 · 시스템 층 inputs · 시험
- 넣을 값 카드(M02_Labor_Values)는 담은 로직 입력 전부를 보임(자리 「선택」 감싸기 없앰) · 이름 「넣을 값」
- 고르기 모달 면적산출 알약 = 복사본 일위대가 알약(가져온 때 그대로)
- 입력값.json · 고른자재.json · /calc 는 그대로 · storage 층 inputs/ 파일은 안 건드림(읽기만 끊음)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
2026-10-04 19:36:46 +09:00

217 lines
8.5 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 { InputsValueHandle } from "../M02_MasterTemplete/M02_Labor_Values";
import type { ValueDoc } from "../M02_MasterTemplete/M02_Labor_Values_Model";
import type { LaborPill, 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 { laborValueData, 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: "면적산출",
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: (saved: boolean) => void): EditView {
const { kind, row, info, items } = req;
let dirty = false;
let saving = false;
let saved = false; // 한 번이라도 저장했나 — 돌아갈 때 「고침 있음」 표시
let doc: unknown = null;
let drawing: DrawingTemplateHandle | null = null;
let sheet: SpreadsheetHandle | null = null;
let card: InputsValueHandle | null = null;
const word = {
structure: TEXT.drawing,
area: TEXT.area,
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 }));
};
/** 복사본 일위대가의 알약(가져온 때 그대로) — 로직 물량 「이름|단위」 + 자재 「자재|MT키」 · 다 고정 */
async function sheetLinks(): Promise<LinkOptions> {
const labor = (await items.read(info.폴더, "일위대가")) as { 알약?: LaborPill[] } | null;
const 항목 = (labor?.알약 ?? []).map((p) => ({
키: p.키,
이름: p.이름,
단위: p.단위,
...(p.자재 ? { 자재: p.자재 } : {}),
고정: true,
}));
if (!항목.length) note.textContent = TEXT.noAreaPill;
return { 항목, 모드: "입력" };
}
async function mountSheet(): Promise<void> {
const [{ createSpreadsheet }, links] = await Promise.all([
import("../A00_Common/spreadsheet/spreadsheet"),
sheetLinks(),
]);
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,
});
}
/** 넣을 값 — 복사본 일위대가 담은 로직 입력 전부(39-1c) */
async function mountInputs(): Promise<void> {
const [{ createInputsValueCard }, labor, values] = await Promise.all([
import("../M02_MasterTemplete/M02_Labor_Values"),
items.read(info.폴더, "일위대가") as Promise<{
줄?: { 담은로직?: { 로직?: string }[] };
} | null>,
items.read(info.폴더, "입력값") as Promise<ValueDoc | null>,
]);
const logics = (labor?.줄?.담은로직 ?? []).map((one) => String(one.로직 ?? "")).filter(Boolean);
card = createInputsValueCard({ mode: "value", onChange: () => setDirty(true) });
card.set(await laborValueData(logics, 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();
} 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);
saved = true;
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(saved);
});
return { root, start: () => void start(), isDirty: () => dirty, leave, destroy };
}