diff --git a/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts b/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts index 3e6a63430..08bcefb1f 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts @@ -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; - /** 일위대가 카드(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 => { if (!laborCard) return; + laborEd?.destroy(); + laborEd = null; + const layer = opts.source().layer; + let mod: typeof import("./M02_MasterTemplete_Labor_Detail"); + let got: Awaited>; 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 { + 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 = { 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(); }, }; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor.css b/M02_MasterTemplete/M02_MasterTemplete_Labor.css index 220c81a2a..3732937a4 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor.css @@ -229,105 +229,6 @@ } } -/* 한 줄 표(카드용 · 40-1) — 금액 = 수량 반영 값 · 반영이 없으면 기준 값 흐리게 */ -.m02-labline { - width: 100%; - font-size: var(--text-caption); -} - -.m02-labline th, -.m02-labline td { - padding: var(--spacing-4) var(--spacing-6, 6px); -} - -.m02-labline th:nth-child(n + 4), -.m02-labline .m02-labrow__num { - text-align: right; - white-space: nowrap; - font-variant-numeric: tabular-nums; -} - -.m02-labline .m02-labrow__name { - display: table-cell; - min-width: 0; - overflow-wrap: anywhere; -} - -.m02-labline .m02-labrow__qty, -.m02-labline .m02-labrow__unit { - color: var(--color-text-secondary); - overflow-wrap: anywhere; -} - -.m02-labline .is-base { - color: var(--color-text-secondary); - font-style: italic; -} - -.m02-labline .m02-labrow__sum td { - font-weight: 600; - border-top: 2px solid var(--color-border); -} - -/* 아주 좁으면(카드 3열 · 400px 이하) 결과 단위 칸을 접음 — 수량산출 글에 단위가 같이 보임 */ -@container m02-labview (max-width: 400px) { - .m02-labline th:nth-child(3), - .m02-labline .m02-labrow__unit { - display: none; - } - - .m02-labline th, - .m02-labline td { - padding: var(--spacing-4) 3px; - } -} - -/* 더 좁으면(카드 320px 안팎 · 340px 이하) 줄마다 두 줄 — 윗줄 항목(3칸) · 수량산출(1칸) · 아랫줄 금액 넷 · - 머리 줄도 같은 꼴(글은 그대로) · 가로 넘침 없음 */ -@container m02-labview (max-width: 340px) { - .m02-labline, - .m02-labline thead, - .m02-labline tbody, - .m02-labline tfoot { - display: block; - width: 100%; - } - - .m02-labline tr { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - border-bottom: 1px solid var(--color-border); - } - - .m02-labline th, - .m02-labline td { - display: block; - min-width: 0; - padding: 2px; - border: 0; - } - - .m02-labline th:first-child, - .m02-labline .m02-labrow__name { - grid-column: span 3; - } - - .m02-labline th:nth-child(2), - .m02-labline .m02-labrow__qty { - text-align: right; - white-space: normal; - } - - .m02-labline th:nth-child(3), - .m02-labline .m02-labrow__unit { - display: none; - } - - .m02-labline .m02-labrow__num { - font-size: 11px; - } -} - /* 수량산출 칸(40-1) — 위 결과단위 기준 수량 · 아래 지금 수량 · 누르면 손 입력 · 엑셀에서 고르기 */ .m02-labrow__qty { white-space: nowrap; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index b72958ff9..5f76ad6da 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -54,6 +54,9 @@ export interface LaborDetailOptions { onChanged: (dirty: boolean) => void; /** 「면적산출에서 잇기」 — 그 열 면적산출 상세로(옮겨 가면 참 · 취소 = 거짓 · 없으면 단추 없음) */ goArea: GoArea | null; + /** 구조물 조합 카드 갈래(43-4) — 단가산출 로직 표 + 자재목록만 · 올리기 · 내리기 · 로직 추가 · + * 정보(단위 · 본뜬) · 다이어그램 없음 · 빼기 · 자재 추가 · 줄 · 수량 모달은 상세와 같음 */ + card?: boolean; } export interface LaborDetailHandle { @@ -103,7 +106,7 @@ export function mountLaborDetail( const matOf = (code: string) => calc?.자재.find((m) => m.코드 === code); const drawDg = (): void => { - if (!dgBox.open) return; + if (opt.card || !dgBox.open) return; // 카드 갈래엔 다이어그램 없음 const data = buildLaborDiagram({ rows: cur.담은로직, logics: [...logics.values()], @@ -129,7 +132,9 @@ export function mountLaborDetail( logicTable( { rows: cur.담은로직, logics, calc, fail, cells: cells() }, // 읽기만이면 순서 · 빼기 없음(줄 누름 = 보기 모달은 둠) - edit ? { open: openRow, qty: openQty, move, remove: removeRow } : { open: openRow }, + edit + ? { open: openRow, qty: openQty, ...(opt.card ? {} : { move }), remove: removeRow } + : { open: openRow }, ), ); matHost.replaceChildren( @@ -384,13 +389,19 @@ export function mountLaborDetail( className: "m01-logic__editor m01-combo m02-labor__edit", attrs: { "data-labor": opt.column }, children: [ - el("section", { - className: "m01lab__box m01-logic__section", - children: [el("div", { className: "m01lab__info", children: [unit.root, origin.root] })], - }), - section(tc("Logics_Title"), addLogic, logicHost), + ...(opt.card + ? [] + : [ + el("section", { + className: "m01lab__box m01-logic__section", + children: [ + el("div", { className: "m01lab__info", children: [unit.root, origin.root] }), + ], + }), + ]), + section(tc("Logics_Title"), opt.card ? null : addLogic, logicHost), section(tc("Labor_Mats"), addMat, matHost), - dgBox, + ...(opt.card ? [] : [dgBox]), ], }), ); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts index 5189b76b1..a0c5500a1 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts @@ -170,7 +170,8 @@ export interface LogicTableData { export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement { const wait = !data.calc && !data.fail; const { move, remove } = on ?? {}; - const tools = !!(move && remove); + // 빼기만(구조물 조합 카드 · 43-4)이어도 끝 칸 — 순서 단추는 `move` 가 있을 때만 + const tools = !!(move || remove); const lineOf = (key: string): LaborCalcLine | undefined => data.calc?.줄.find((l) => l.로직 === key); const trs = data.rows.map((row, i) => { @@ -222,14 +223,15 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement qtyCell(line, data.cells, on?.qty ? () => on.qty?.(i) : null), el("td", { className: "m02-labrow__unit", text: unit }), ...COSTS.map((c) => moneyCell(line?.기준?.[c], line?.반영?.[c], wait, tc(HEAD[c]))), - ...(move && remove + ...(tools ? [ el("td", { className: "m02-labrow__move", children: [ - small(tc("Up"), () => move(i, -1)), - small(tc("Down"), () => move(i, 1)), - small(tc("Remove"), () => remove(i)), + ...(move + ? [small(tc("Up"), () => move(i, -1)), small(tc("Down"), () => move(i, 1))] + : []), + ...(remove ? [small(tc("Remove"), () => remove(i))] : []), ], }), ] @@ -361,115 +363,3 @@ export function matTable(data: MatTableData, on?: Omit): HT ? table(heads, trs, []) : el("p", { className: "m02-labrow__muted", text: tc("Labor_NoMat") }); } - -export interface CompactData { - rows: LaborRow[]; - mats: LaborMat[]; - logics: Map; - calc: LaborCalc | null; - fail?: string | null; - cells?: ReadonlyMap; -} - -/** 한 줄 표(카드용 · 40-1) — 항목 · 수량산출 · 단위 · 노무비 · 재료비 · 경비 · 합계 · 금액 = 수량 반영 값 · - * 반영이 없으면 기준 값 흐리게(`is-base`) · 자재 줄은 같은 표 아래(재료비 · 합계만) · 맨 아래 합계 줄 */ -export function compactTable(data: CompactData): HTMLElement { - const wait = !data.calc && !data.fail; - const cell = (v: number | null | undefined, base: boolean, label: string): HTMLElement => - el("td", { - className: `m02-labrow__num${base ? " is-base" : ""}`, - text: wait ? "…" : v === null || v === undefined ? "—" : formatMoney(v), - attrs: { "data-label": label, ...(base ? { title: tc("Labor_Base") } : {}) }, - }); - // 머리 글 = 사용자 글 그대로(40-1 · 항목 · 수량산출 · 단위 · 노무비 · 재료비 · 경비 · 합계) - const heads = [ - tc("Labor_Item"), - tc("Labor_QtyCol"), - tc("Labor_Unit"), - ...COSTS.map((c) => tc(HEAD[c])), - ]; - const lines = data.rows.map((row) => { - const line = data.calc?.줄.find((l) => l.로직 === row.로직); - const money = line?.반영 ?? line?.기준; - const base = !line?.반영 && !!line?.기준; - const logic = data.logics.get(row.로직); - return el("tr", { - className: "m02-labrow__row", - attrs: { "data-labor-logic": row.로직 }, - children: [ - el("td", { - className: "m02-labrow__name", - text: logic?.이름 ?? row.로직, - attrs: { title: `${row.로직}${line?.까닭 ? ` · ${line.까닭}` : ""}` }, - }), - el("td", { - className: "m02-labrow__qty", - text: qtyShort(line, line?.분모 ?? "", data.cells), - attrs: { "data-label": tc("Labor_QtyCol") }, - }), - el("td", { className: "m02-labrow__unit", text: logic?.결과단위 ?? "" }), - ...COSTS.map((c) => cell(money?.[c], base, tc(HEAD[c]))), - ], - }); - }); - const mats = data.mats.map((mat) => { - const line = data.calc?.자재.find((m) => m.코드 === mat.코드); - const has = line?.반영 !== null && line?.반영 !== undefined; - const v = has ? line?.반영 : line?.기준단가; - return el("tr", { - className: "m02-labrow__row is-mat", - attrs: { "data-labor-mat": mat.코드 }, - children: [ - el("td", { - className: "m02-labrow__name", - text: mat.이름 ?? mat.코드, - attrs: { title: [mat.코드, mat.규격].filter(Boolean).join(" · ") }, - }), - el("td", { - className: "m02-labrow__qty", - text: qtyShort(line, mat.단위 ?? "", data.cells), - attrs: { "data-label": tc("Labor_QtyCol") }, - }), - el("td", { className: "m02-labrow__unit", text: mat.단위 ?? "" }), - el("td", { className: "m02-labrow__num", attrs: { "data-label": tc(HEAD.노무비) } }), - cell(v, !has && v !== null && v !== undefined, tc(HEAD.재료비)), - el("td", { className: "m02-labrow__num", attrs: { "data-label": tc(HEAD.경비) } }), - cell(v, !has && v !== null && v !== undefined, tc(HEAD.계)), - ], - }); - }); - const applied = data.calc?.반영합; - const anyApplied = !!applied && (applied.계 ?? 0) !== 0; - const total = anyApplied ? applied : data.calc?.기준합; - const foot = - data.rows.length + data.mats.length - ? [ - el("tr", { - className: "m02-labrow__sum", - children: [ - el("td", { className: "m02-labrow__name", text: tc("Prev_Sum") }), - el("td", { className: "m02-labrow__qty", text: "" }), - el("td", { className: "m02-labrow__unit", text: "" }), - ...COSTS.map((c) => cell(total?.[c], !anyApplied && !!total, tc(HEAD[c]))), - ], - }), - ] - : []; - return data.rows.length + data.mats.length - ? el("div", { - className: "ui-general-block__table-wrap m02-labcompact", - children: [ - el("table", { - className: "ui-general-block__table m02-labline", - children: [ - el("thead", { - children: [el("tr", { children: heads.map((t) => el("th", { text: t })) })], - }), - el("tbody", { children: [...lines, ...mats] }), - ...(foot.length ? [el("tfoot", { children: foot })] : []), - ], - }), - ], - }) - : el("p", { className: "m02-labrow__muted", text: tc("NoLogic") }); -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_View.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_View.ts index 517e9efcc..95a6aae82 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_View.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_View.ts @@ -1,12 +1,11 @@ /* ============================================================================= * M02_MasterTemplete_Labor_View.ts * 일위대가 보기 갈래(39-3 · 고치기 없음) — 그 열 일위대가의 담은 로직 표 + 자재목록 표(기준 · 반영). - * 면적산출 상세 오른쪽 패널(sub3) · 구조물 조합 일위대가 카드(sub1)가 씀. 표 모양은 상세와 한 벌(`Labor_Rows`). + * 면적산출 상세 오른쪽 패널(sub3)이 씀(구조물 조합 카드는 43-4 부터 `Labor_Detail` 카드 갈래). 표 모양은 상세와 한 벌(`Labor_Rows`). * · `show(층, 열, 수량?)` = 문서 · 로직 입력 목록 · calc 를 받아 그림(최신 응답만) * · `setQty({알약 키: 칸 값}, {알약 키: 칸 주소}?)` = 면적 칸 값이 바뀌면 서버 없이 짝 셈(`Labor_Calc.settle`)으로 * 바로 곱함 · 칸 주소를 주면 저장 전 연결(setLink · 알약 끌기)도 「수량 B6 = 20」 으로 보임(안 주면 저장본 연결) * (그 열 면적 알약 값 전부를 줌 — 빠진 키는 「수량 없음」) - * · `compact` = 한 줄 표(구조물 조합 일위대가 카드 · 40-1) * · `onRow` = 줄 누름(알약 키 · 이름 · 단위 — 칸 지정 모달 등) · 없으면 누름 없음 * ========================================================================== */ @@ -21,7 +20,7 @@ import { type LaborDoc, } from "./M02_MasterTemplete_Labor_Api"; import { settle } from "./M02_MasterTemplete_Labor_Calc"; -import { compactTable, logicTable, matTable } from "./M02_MasterTemplete_Labor_Rows"; +import { logicTable, matTable } from "./M02_MasterTemplete_Labor_Rows"; import { tc } from "./M02_MasterTemplete_Labor_Text"; import "./M02_MasterTemplete_Labor.css"; @@ -47,15 +46,7 @@ export interface LaborViewHandle { destroy: () => void; } -/** `compact` = 한 줄 표(카드용 · 항목 · 수량산출 · 단위 · 금액 넷 · 자재 줄 같은 표 아래 · 줄 누름 없음) */ -export interface LaborViewOptions { - compact?: boolean; -} - -export function createLaborView( - onRow?: (row: LaborViewRow) => void, - opts: LaborViewOptions = {}, -): LaborViewHandle { +export function createLaborView(onRow?: (row: LaborViewRow) => void): LaborViewHandle { const root = el("div", { className: "m02-labview" }); const logics = new Map(); let doc: LaborDoc | null = null; @@ -93,10 +84,6 @@ export function createLaborView( unit: mat.단위 ?? "", })?.(); }; - if (opts.compact) - return void root.replaceChildren( - compactTable({ rows: d.담은로직, mats: d.자재, logics, calc, fail, cells }), - ); root.replaceChildren( el("h4", { className: "m02-labview__title", text: tc("Logics_Title") }), logicTable( diff --git a/resources/tester/test_m02_card_shell.py b/resources/tester/test_m02_card_shell.py index aae597a1d..1b7c067a6 100644 --- a/resources/tester/test_m02_card_shell.py +++ b/resources/tester/test_m02_card_shell.py @@ -61,3 +61,22 @@ def test_실패는_카드_안_글과_다시_불러오기(): assert "const retry = (): void => void paintLabor(row, seq);" in picker assert "inputs.set(null, why(error), retry);" in picker assert "const stops = [labor.loading(), inputs.loading()];" in picker + + +def test_일위대가_카드는_상세_카드_갈래(): + """43-4 — 일위대가 카드 몸 = Labor_Detail 카드 갈래(단가산출 로직 2행 표 + 자재목록 · 순서 · + 로직 추가 · 다이어그램 없음 · 빼기 그대로) · 카드 [저장] = 면적 먼저 → 일위대가 · + 한 줄 표 지움.""" + cards = _text("M02_MasterTemplete_ItemCards.ts") + assert "mod.mountLaborDetail(laborHost, got.문서, {" in cards and "card: true," in cards + assert 'makeCard("labor", PART_WORD.labor, laborHost, () => saveLabor())' in cards + save = cards[cards.index("async function saveLabor()") :] + assert save.index("laborEd.saveArea()") < save.index('saveTemplate(layer, "labor"') + assert "onChanged: (dirty) => laborCard.saver.mark(dirty && opts.editable())," in cards + assert "Labor_View" not in cards # 한 줄 표(40-2) 안 씀 + detail = _text("M02_MasterTemplete_Labor_Detail.ts") + assert "...(opt.card ? {} : { move })" in detail # 순서 단추 없음 · 빼기는 그대로 + assert "opt.card ? null : addLogic" in detail and "...(opt.card ? [] : [dgBox])," in detail + rows = _text("M02_MasterTemplete_Labor_Rows.ts") + assert "const tools = !!(move || remove);" in rows and "compactTable" not in rows + assert "compact" not in _text("M02_MasterTemplete_Labor_View.ts").split("*/", 1)[1] diff --git a/resources/tester/ui_checks/43-4_일위대가_카드.js b/resources/tester/ui_checks/43-4_일위대가_카드.js new file mode 100644 index 000000000..bc992ab31 --- /dev/null +++ b/resources/tester/ui_checks/43-4_일위대가_카드.js @@ -0,0 +1,96 @@ +/* 43-4 — 구조물 조합 일위대가 카드 = 일위대가 상세 카드 갈래(단가산출 로직 2행 표 + 자재목록) + * 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=구조물조합&root=master) · 시스템 관리자 · + * 나무에 item1(일위대가 담은 로직 있는 열)이 있음 + * 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌 · 20초 안 — 길면 ORCA 가 끊음) + * 기대 수치: + * - sections = ["단가산출 로직", "자재목록"] · diagram = 0 · info = 0(단위 · 본뜬 칸 없음) + * - addLogic = 0 · up = down = 0 · remove = 로직 줄 수 · addMat = 1 · rows = 담은 로직 수(item1 = 14) + * - money2 = true(금액 칸이 두 줄 — 위 기준 · 아래 반영) + * - scroll.overflowX = "auto" · scroll.wide = 참(item1 표 ≈ 870px > 카드 ≈ 310px · 표 감싸개 안 가로 스크롤) + * - rowModal = 「넣을 값 — …」 · qtyModal = 모달 뜸(수량 · 43-1) · 둘 다 닫기 뒤 dirty = false + * - 빼기 하나 → dirty = true · saveEnabled = true · 다른 항목(item3) → 버림 물음 「확인」 → item1 다시(되돌림) + * → rowsAfter = rows · dirtyAfter = false + */ +(async () => { + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 6000) => { + for (let t = 0; t < ms; t += 150) { + const v = fn(); + if (v) return v; + await w(150); + } + return null; + }; + const vis = (b) => b && !b.hidden && b.offsetParent !== null; + const pick = async () => { + document.querySelector('[data-node="i|item1"]').click(); + return until(() => { + const ed = document.querySelector(".m02-struct__labor .m02-labor__edit"); + return ed && ed.querySelector("tbody tr[data-labor-logic]") ? ed : null; + }); + }; + const ed = await pick(); + if (!ed) return JSON.stringify({ error: "카드 몸 없음" }); + const card = ed.closest(".m02-struct > *"); + const btn = (t) => + [...card.querySelectorAll("button")].filter((b) => vis(b) && b.textContent === t); + const save = [...card.querySelectorAll(".ui-collapsible__title button")].find( + (b) => b.textContent === "저장", + ); + const rows = () => ed.querySelectorAll("tbody tr[data-labor-logic]").length; + const money = ed.querySelector("tbody tr[data-labor-logic] td:nth-child(5)"); + const out = { + sections: [...ed.querySelectorAll(".m01-logic__section-head h3")].map((h) => h.textContent), + diagram: ed.querySelectorAll(".m02-labor__dg").length, + info: ed.querySelectorAll("[data-labor-field]").length, + addLogic: btn("로직 추가").length, + up: btn("올리기").length, + down: btn("내리기").length, + remove: btn("빼기").length, + addMat: btn("자재 추가").length, + rows: rows(), + money2: !!money && money.children.length >= 2, + // 표마다 공용 감싸개(ui-general-block__table-wrap)가 가로 스크롤 — 카드보다 넓으면 그 안에서 + scroll: (() => { + const wrap = ed.querySelector("table")?.parentElement; + return wrap + ? { + overflowX: getComputedStyle(wrap).overflowX, + wide: wrap.scrollWidth > wrap.clientWidth, + sw: wrap.scrollWidth, + cw: wrap.clientWidth, + } + : null; + })(), + }; + // 줄 누름 = 넣을 값 모달 · 수량산출 칸 = 수량 모달 — 닫기만(값 안 바꿈) + const modal = async (open) => { + open(); + const m = await until(() => document.querySelector(".ui-modal"), 2000); + const t = m?.querySelector("h3")?.textContent ?? null; + [...(m?.querySelectorAll("button") ?? [])].find((b) => b.textContent === "닫기")?.click(); + await until(() => !document.querySelector(".ui-modal"), 1500); + return t; + }; + const first = () => ed.querySelector("tbody tr[data-labor-logic]"); + out.rowModal = await modal(() => first().querySelector(".m02-labrow__name").click()); + out.qtyModal = await modal(() => + ( + first().querySelector("[data-labor-qty-open]") ?? first().querySelector(".m02-labrow__qty") + ).click(), + ); + out.dirtyAfterModals = !save.disabled; + // 빼기 하나 → 고침 표시 · 다른 항목으로 갔다 버림 확인 → item1 다시(서버 판으로 되돌림) + btn("빼기").pop().click(); + await w(300); + out.dirty = card.classList.contains("is-dirty") || !save.disabled; + out.saveEnabled = !save.disabled; + document.querySelector('[data-node="i|item3"]').click(); + const ask = await until(() => document.querySelector(".ui-confirm"), 2000); + [...(ask?.querySelectorAll("button") ?? [])].find((b) => b.textContent === "확인")?.click(); + await w(600); + const again = await pick(); + out.rowsAfter = again ? again.querySelectorAll("tbody tr[data-labor-logic]").length : null; + out.dirtyAfter = !save.disabled; + return JSON.stringify(out); +})();