diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor.css b/M02_MasterTemplete/M02_MasterTemplete_Labor.css index e4f13d0f6..a66f2a6ac 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor.css @@ -21,13 +21,25 @@ background: var(--color-mist-violet); } +/* 이름 칸(45-7) — 칸은 표 칸 그대로(flex 면 칸 선이 끊김) · 코드 · 이름 · 설명은 글마다 한 줄 말줄임(전체 = title) */ .m02-labrow__name { - display: flex; - flex-direction: column; - gap: 2px; min-width: 12rem; } +.m02-labrow__name > * { + display: block; + max-width: 20rem; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.m02-labrow th, +.m02-labrow__code, +.m02-labrow__spec { + white-space: nowrap; +} + .m02-labrow__key, .m02-labrow__note, .m02-labrow__muted { @@ -129,9 +141,9 @@ border-style: solid; } -/* 총합계 줄 전체 합계 칸(44-6) — 테두리 · 색 */ +/* 총합계 줄 전체 합계 칸(44-6) — 테두리 · 색 · 테두리는 안쪽 그림자(칸 선 굵기 안 바꿈 · 45-7) */ .m02-labrow__sum .m02-labrow__total { - border: 2px solid var(--color-primary); + box-shadow: inset 0 0 0 2px var(--color-primary); background: var(--color-mist-violet); color: var(--color-primary); } @@ -232,6 +244,10 @@ min-width: 0; } + .m02-labrow .m02-labrow__name > * { + max-width: none; + } + .m02-labrow .m02-labrow__no, .m02-labrow .m02-labrow__unit:not([data-label]), .m02-labrow .m02-labrow__code, diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts index 657c629ec..4047c4ac4 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts @@ -135,6 +135,12 @@ function moneyCell( }); } +/** 이름 칸 글마다 title = 전체 글(한 줄 말줄임 · 45-7) */ +const titled = (td: HTMLElement): HTMLElement => { + for (const c of td.children) c.setAttribute("title", c.textContent ?? ""); + return td; +}; + const table = (heads: string[], rows: HTMLElement[], foot: HTMLElement[]): HTMLElement => el("div", { className: "ui-general-block__table-wrap", @@ -240,30 +246,32 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement attrs: { "data-labor-logic": row.로직 }, children: [ el("td", { className: "m02-labrow__no", text: String(i + 1) }), - el("td", { - className: "m02-labrow__name", - children: [ - el("span", { className: "m02-labrow__key", text: row.로직 }), - el("strong", { text: logic?.이름 ?? row.로직 }), - ...(values.empty - ? [ - el("small", { - className: "m02-labrow__empty", - text: tc("Labor_Empty").replace("{n}", String(values.empty)), - }), - ] - : []), - ...(row.메모 ? [el("small", { className: "m02-labrow__note", text: row.메모 })] : []), - ...(why - ? [ - el("small", { - className: "m02-labrow__empty", - text: `${tc("Labor_Calc_Fail")}: ${why}`, - }), - ] - : []), - ], - }), + titled( + el("td", { + className: "m02-labrow__name", + children: [ + el("span", { className: "m02-labrow__key", text: row.로직 }), + el("strong", { text: logic?.이름 ?? row.로직 }), + ...(values.empty + ? [ + el("small", { + className: "m02-labrow__empty", + text: tc("Labor_Empty").replace("{n}", String(values.empty)), + }), + ] + : []), + ...(row.메모 ? [el("small", { className: "m02-labrow__note", text: row.메모 })] : []), + ...(why + ? [ + el("small", { + className: "m02-labrow__empty", + text: `${tc("Labor_Calc_Fail")}: ${why}`, + }), + ] + : []), + ], + }), + ), aliasCell(row.별칭, on?.alias ? (v) => on.alias?.(i, v) : null), qtyCell(line, data.cells, on?.qty ? () => on.qty?.(i) : null), el("td", { className: "m02-labrow__unit", text: unit }), @@ -382,16 +390,22 @@ export function matTable(data: MatTableData, on?: Omit): HT attrs: { "data-labor-mat": mat.코드 }, children: [ el("td", { className: "m02-labrow__code", text: mat.코드 }), + titled( + el("td", { + className: "m02-labrow__name", + children: [ + el("strong", { text: mat.이름 ?? "" }), + ...(line?.까닭 + ? [el("small", { className: "m02-labrow__empty", text: line.까닭 })] + : []), + ], + }), + ), el("td", { - className: "m02-labrow__name", - children: [ - el("strong", { text: mat.이름 ?? "" }), - ...(line?.까닭 - ? [el("small", { className: "m02-labrow__empty", text: line.까닭 })] - : []), - ], + className: "m02-labrow__spec", + text: mat.규격 ?? "", + attrs: { title: mat.규격 ?? "" }, }), - el("td", { className: "m02-labrow__spec", text: mat.규격 ?? "" }), qtyCell(qtyLine, data.cells, on?.qty ? () => on.qty?.(i) : null), el("td", { className: "m02-labrow__unit", diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index e48eaa057..eeadef7f7 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -39,3 +39,12 @@ ## 추가 (10-5 · 원문 그대로) 다음으로 구조물 조합 분류 테이블과 상세컨테이너사이가 공간이 너무 넣어 그리고 구조물 분류 테이블의 하단에 구분선이 있는것같은데 삭제해줘. 컨테이너의 상단에도 구분선이 있어. + +## 추가 2 (10-5 · 원문 그대로) + +1. 구조물 조합의 ?버튼은 설명같은데 미리보기의 ? 버튼을 미리보기 항목 옆으로 이동하고 설명과 지금처럼 단축키와 기능 설명과 사용법을 작성해줘. +2. 구조물 조합의 파일 불러오기 기능은 미리보기에서 보다는 상세 페이지에서 작업할 내용임. 미리보기에서는 항목 삭제 +3. M01의 단가산출 로직의 항목 상세에서 우측 패널에 대한 구현이 잘못됨. 우측 흐름도의 패널과 원문 패널을 별도로 구성하고 사용자가 두개의 패널을 각각 접었다 펼수 있어야함. 단 동시에 두개의 패널을 볼 경우는 없으며 폭정보는 공유하고 있고 둘다 접힌 상태일 수는 있음. +4. 구조물 조합의 단가산출 상세화면에도 원문보기 우측 패널이 있어야함. 우측 패널은 단가산출 로직을 선택할 경우에 원문을 보여주는 기능이며 패널을 접었다 펼수 있어야함. +5. 원문 패널 추가에 따라 현재 단가산출 로직 항목을 선택하면 옵션창이 보이나 선택하면 하이라이트를 하고 원문을 보일수 있게 함. 그리고 옵션설정에 대한 조정은 올리기, 내리기, 빼기 버튼의 이름을 위로, 아래로, 삭제로 변경하고 아래로 와 삭제 사이에 신규 버튼(이름 : 옵션)을 추가하여 선택할 수 있게 변경. 버튼 사이간격에 대해 템플릿화하여 적용. +6. 구조물 조합의 면적산출 우측패널에 별도의 도면 패널 추가(기능은 4번 요청 확인) diff --git a/resources/tester/ui_checks/45-7_일위대가_표_줄.js b/resources/tester/ui_checks/45-7_일위대가_표_줄.js new file mode 100644 index 000000000..04b66d735 --- /dev/null +++ b/resources/tester/ui_checks/45-7_일위대가_표_줄.js @@ -0,0 +1,100 @@ +/* 45-7 — M02 구조물 조합 · 일위대가 표(단가산출 로직 · 자재목록) 이름 한 줄 · 칸 선 이어짐 — 카드 · 상세 두 곳 + * 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=구조물조합&root=master) · 시스템 관리자 · + * 나무에 item1(구조물 조합 · 일위대가 로직 담김)이 있음 + * 부르는 법: orca eval --expression "$(tr '\n' ' ' < 이 파일)" (async 식 · JSON 글을 돌려줌 · 줄바꿈 있으면 orca 가 끊김) + * 하는 일: item1 카드 표를 잼 → 일위대가 카드 [수정]으로 상세를 열어 잼 → item1 을 다시 눌러 카드 화면으로 돌아감(값 안 바꿈) + * 기대 수치(카드 · 상세 각각): + * - nameLines = 1(이름 칸 · 코드 · 규격 칸 글마다 줄 수 최대) · headLines = 1(머리 칸 글 줄 수 최대) + * - spanBad = 0(각 줄 칸 수 = 머리 칸 수 · colspan 셈) + * - broken = 0(칸 위·아래가 줄 위·아래와 어긋남 · 한 줄 안 아래 테두리 굵기가 다름 · 칸 사이 틈) + * - ellipsis = 이름 글에 말줄임 꼴(text-overflow ellipsis) · titled = 이름 글마다 title + * - 카드는 가로 스크롤 그대로(cardScroll = true · 표 폭 > 감싼 칸 폭) + */ +(async () => { + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 8000) => { + for (let t = 0; t < ms; t += 150) { + const v = fn(); + if (v) return v; + await w(150); + } + return null; + }; + const lines = (e) => { + const r = document.createRange(); + r.selectNodeContents(e); + const tops = [...r.getClientRects()].filter((q) => q.width > 0).map((q) => Math.round(q.top)); + return new Set(tops).size; + }; + const span = (tr) => [...tr.cells].reduce((a, c) => a + (c.colSpan || 1), 0); + const measure = (t) => { + const head = span(t.tHead.rows[0]); + const body = [...t.querySelectorAll("tbody tr, tfoot tr")]; + const texts = [ + ...t.querySelectorAll( + ".m02-labrow__name > *, tbody .m02-labrow__code, tbody .m02-labrow__spec", + ), + ...t.querySelectorAll(".m02-labrow__name:not(:has(*))"), + ].filter((e) => e.textContent.trim()); + const kids = [...t.querySelectorAll(".m02-labrow__name > *")]; + let broken = 0; + const why = []; + for (const tr of body) { + const R = tr.getBoundingClientRect(); + const cells = [...tr.cells].filter((c) => c.getClientRects().length); + const widths = new Set(cells.map((c) => getComputedStyle(c).borderBottomWidth)); + if (widths.size > 1) (broken++, why.push(`굵기 ${tr.className} ${[...widths]}`)); + cells.forEach((c, i) => { + const r = c.getBoundingClientRect(); + if (Math.abs(r.top - R.top) > 1 || Math.abs(r.bottom - R.bottom) > 1) + (broken++, + why.push(`높이 ${tr.className} ${c.className} ${Math.round(r.bottom - R.bottom)}`)); + const n = cells[i + 1]; + if (n && Math.abs(n.getBoundingClientRect().left - r.right) > 1) + (broken++, why.push(`틈 ${tr.className} ${c.className}`)); + }); + } + const wrap = t.parentElement; + return { + head, + rows: body.length, + spanBad: body.filter((tr) => span(tr) !== head).length, + nameLines: Math.max(0, ...texts.map(lines)), + headLines: Math.max(0, ...[...t.tHead.rows[0].cells].map(lines)), + broken, + why: why.slice(0, 6), + ellipsis: kids.every((e) => getComputedStyle(e).textOverflow === "ellipsis"), + titled: kids.every((e) => e.title === e.textContent), + scroll: Math.round(t.getBoundingClientRect().width) > Math.round(wrap.clientWidth), + }; + }; + const node = await until(() => document.querySelector('[data-node="i|item1"]')); + if (!node) return JSON.stringify({ error: "item1 없음 — 기슭막이 폴더를 펼쳐 둘 것" }); + node.click(); + const card = await until(() => + [...document.querySelectorAll(".m02-struct__card")].find((c) => + c.querySelector("table.m02-labrow"), + ), + ); + if (!card) return JSON.stringify({ error: "일위대가 카드 표 없음" }); + await w(1500); + const out = { card: [...card.querySelectorAll("table.m02-labrow")].map(measure) }; + out.cardScroll = out.card[0]?.scroll ?? null; + const edit = [...card.querySelectorAll(".m02-struct__card-tools button")].find( + (b) => b.innerText.trim() === "수정", + ); + if (!edit) return JSON.stringify({ ...out, error: "일위대가 [수정] 없음" }); + edit.click(); + await until(() => + [...document.querySelectorAll("table.m02-labrow")].find((t) => !t.closest(".m02-struct__card")), + ); + await w(2000); + out.detail = [...document.querySelectorAll("table.m02-labrow")] + .filter((t) => !t.closest(".m02-struct__card")) + .map(measure); + /* 카드 화면으로 돌아감 */ + document.querySelector('[data-node="i|item1"]')?.click(); + await w(1000); + out.back = !!document.querySelector(".m02-struct__card table.m02-labrow"); + return JSON.stringify(out); +})();