fix(M02): 일위대가 표 이름 한 줄 말줄임 · 칸 선 이어짐(45-7)

- 이름 칸 flex 뺌(칸 선 끊김 원인) · 코드 · 이름 · 설명 글마다 한 줄 말줄임 + title
- 머리 칸 · 코드 · 규격 줄바꿈 없음 · 전체 합계 칸 테두리를 안쪽 그림자로(칸 선 굵기 그대로)
- 확인 시나리오 resources/tester/ui_checks/45-7_일위대가_표_줄.js

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
This commit is contained in:
2026-10-05 13:13:57 +09:00
co-authored by Claude Opus 5.5
parent aa2845eef7
commit 2334a2abcf
3 changed files with 167 additions and 37 deletions
@@ -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,
@@ -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<RowHandlers, "move">): 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",
@@ -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);
})();