feat(M02): 구조물 조합 일위대가 카드 = 상세 그대로 — 단가산출 로직 2행 표 + 자재목록(Labor_Detail 카드 갈래 · 순서 · 로직 추가 · 다이어그램 · 정보 칸 없음 · 빼기 · 자재 추가 그대로) · 카드 [저장] · Ctrl+S

43-4 — 줄 누름 = 넣을 값 모달 · 수량산출 칸 = 수량 모달(상세와 같은 함수) · 저장 차례 = 면적 먼저 → 일위대가 · 표 감싸개 안 가로 스크롤 · 40-2 한 줄 표(compactTable · Labor_View compact · m02-labline css) 지움 · 빼기만 있는 줄도 끝 칸 · 화면 시나리오 ui_checks/43-4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
This commit is contained in:
2026-10-05 00:33:55 +09:00
co-authored by Claude Opus 5.5
parent 31a753b7fd
commit ca4fcf0364
7 changed files with 195 additions and 262 deletions
@@ -19,7 +19,12 @@ import type {
SpreadsheetHandle,
Workbook,
} from "../A00_Common/spreadsheet/spreadsheet";
import type { StructureDoc, StructureNumbers } from "./M02_MasterTemplete_Api_Fetch";
import {
readTemplate,
saveTemplate,
type StructureDoc,
type StructureNumbers,
} from "./M02_MasterTemplete_Api_Fetch";
import {
cardShell,
helpButton,
@@ -35,7 +40,7 @@ import {
type DrawingCardHandle,
} from "./M02_MasterTemplete_DrawingCard";
import type { ItemPart, ItemSource } from "./M02_MasterTemplete_ItemSource";
import type { LaborViewHandle } from "./M02_MasterTemplete_Labor_View";
import type { LaborDetailHandle } from "./M02_MasterTemplete_Labor_Detail";
import "./M02_MasterTemplete_Structure.css";
/** 집계표 열 하나에 딸린 문서 셋 — 번호 칸 · 이름 */
@@ -124,7 +129,7 @@ export interface ItemCardsOptions {
/** 카드 [삭제](40-2) — 그 항목 비우기(번호 · 열 그대로 · 물음은 카드가 한 번) · 없으면 단추 없음 ·
* 끝나면 카드를 다시 읽음(빈 카드 = [새로 만들기] [복사 생성]) */
onClear?: (kind: CardKind) => Promise<void>;
/** 일위대가 카드(39-5 · 몸 = 일위대가 보기 갈래 `Labor_View`) — 도면 다음 · 지금 층에서 읽음 ·
/** 일위대가 카드(39-5 · 몸 = 일위대가 상세 카드 갈래 `Labor_Detail` · 43-4) — 도면 다음 · 지금 층에서 읽음 ·
* [일위대가 수정] · 빈 카드 [새로 만들기] = 이것(일위대가 상세) */
labor?: () => void;
}
@@ -327,35 +332,57 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
});
paintPills(readStateRaw("m02-card-pills") === "true");
sheets[0].shell.tools.prepend(pills);
// 일위대가 카드 — 보기만(고침 = [일위대가 수정] 상세) · 부품은 처음 열 때 받음
// 일위대가 카드(43-4) — 상세의 단가산출 로직 표(2행) + 자재목록 그대로(`Labor_Detail` 카드 갈래 ·
// 순서 · 로직 추가 · 다이어그램 없음) · 고침은 카드 [저장] · Ctrl+S · 부품은 처음 열 때 받음
const laborHost = el("div", { className: "m02-struct__labor" });
const laborCard = opts.labor
? makeCard("labor", PART_WORD.labor, laborHost, () => Promise.resolve())
? makeCard("labor", PART_WORD.labor, laborHost, () => saveLabor())
: null;
let laborView: LaborViewHandle | null = null;
/** 일위대가 카드 — 그 열 문서 · calc 를 받아 그림 · 빈 일위대가(로직 · 자재 없음)면 두 단추 */
let laborEd: LaborDetailHandle | null = null;
let laborVer = "";
/** 일위대가 카드 — 그 열 문서를 읽어 카드 갈래로 붙임 · 빈 일위대가(로직 · 자재 없음)면 두 단추 */
const paintLabor = async (at: string, mine: number): Promise<void> => {
if (!laborCard) return;
laborEd?.destroy();
laborEd = null;
const layer = opts.source().layer;
let mod: typeof import("./M02_MasterTemplete_Labor_Detail");
let got: Awaited<ReturnType<typeof readTemplate>>;
try {
// 카드 = 한 줄 표(항목 · 수량산출 · 단위 · 금액 넷 · 40-2) — 자세한 것은 [수정] 상세
laborView ??= (await import("./M02_MasterTemplete_Labor_View")).createLaborView(undefined, {
compact: true,
});
[mod, got] = await Promise.all([
import("./M02_MasterTemplete_Labor_Detail"),
readTemplate(layer, "labor", at, null),
]);
} catch (error) {
if (mine === seq) laborCard.shell.fail(fill(TEXT.noSheet, why(error)), reload);
return;
}
if (mine !== seq) return;
const view = laborView;
laborHost.replaceChildren(view.root);
await view.show(opts.source().layer, at);
if (mine !== seq) return;
const d = view.doc();
laborVer = got.판 ?? "";
const d = (got.문서 ?? {}) as { 일위대가번?: string; 담은로직?: unknown[]; 자재?: unknown[] };
laborCard.no.textContent =
d?.일위대가번 || numberOf("labor", at) || fill(TEXT.noFieldNumber, PART_FIELD.labor);
if (blank(laborCard, !!d && !d.담은로직.length && !d.자재.length))
laborHost.replaceChildren(startBox(laborCard, () => opts.labor?.()));
d.일위대가번 || numberOf("labor", at) || fill(TEXT.noFieldNumber, PART_FIELD.labor);
if (blank(laborCard, !d.담은로직?.length && !d.자재?.length))
return void laborHost.replaceChildren(startBox(laborCard, () => opts.labor?.()));
laborEd = mod.mountLaborDetail(laborHost, got.문서, {
layer,
column: at,
readOnly: !opts.editable(),
onChanged: (dirty) => laborCard.saver.mark(dirty && opts.editable()),
goArea: null, // 면적 칸 잇기는 수량 모달 안(43-1) — 카드에서 화면을 옮기지 않음
card: true,
});
};
/** 카드 [저장] — 상세와 같은 차례: 고른 칸 연결(면적 문서) 먼저 → 일위대가(판을 다시 읽어 409 막음) */
async function saveLabor(): Promise<void> {
const at = column;
if (!laborEd || !at) return;
const layer = opts.source().layer;
const areaSaved = await laborEd.saveArea();
if (areaSaved) laborVer = (await readTemplate(layer, "labor", at, null)).판 ?? "";
laborVer = (await saveTemplate(layer, "labor", at, null, laborVer, laborEd.getDoc())).판;
if (areaSaved) window.setTimeout(reload, 0); // 면적산출 카드도 새 연결로
}
const cards: Card[] = [info, ...(laborCard ? [laborCard] : []), ...sheets];
const defaults: Record<CardKind, string> = {
structure: TEXT.edit,
@@ -375,8 +402,10 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
const drawEdit = drawingEditable(opts.editable(), opts.source());
const sheetEdit = !!opts.sheetEdit?.can();
for (const c of cards) {
const can = c.kind === "structure" ? drawEdit : c.kind !== "labor" && sheetEdit;
const can =
c.kind === "structure" ? drawEdit : c.kind === "labor" ? opts.editable() : sheetEdit;
c.saver.button.hidden = c.help.hidden = !can;
if (c.kind === "labor") c.help.hidden = true; // 일위대가 카드엔 단축키 상자 없음
c.edit.hidden = !opts.editable();
c.del.hidden = !opts.editable() || !opts.onClear; // 빈 카드는 그린 뒤 `blank` 가 다시 숨김
}
@@ -533,7 +562,7 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
}
const source = opts.source();
// 0.3초 넘으면 카드마다 칸 안 「불러오는 중」(PLAN 30-6 · 31-7) — 일위대가 카드는 뺌:
// 몸(Labor_View)은 따로 그려지고 제 「불러오는 중」 이 있음 · 넣으면 덮였다 지워져 빈 카드(40 D1)
// 몸(일위대가 상세 카드 갈래)은 따로 읽어 그림(금액 「…」 이 기다림 표시) · 넣으면 덮였다 지워져 빈 카드(40 D1)
const stops = cards.filter((c) => c !== laborCard).map((c) => c.shell.loading());
const [drawing, ...books] = await Promise.allSettled([
source.read("structure", col.id),
@@ -582,7 +611,7 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
destroy: () => {
seq += 1;
drop();
laborView?.destroy();
laborEd?.destroy();
root.remove();
},
};