Merge remote-tracking branch 'origin/main_laptop_1' into sub_desktop_1

This commit is contained in:
2026-10-05 13:50:07 +09:00
4 changed files with 176 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",
@@ -39,3 +39,12 @@
## 추가 (10-5 · 원문 그대로)
다음으로 구조물 조합 분류 테이블과 상세컨테이너사이가 공간이 너무 넣어 그리고 구조물 분류 테이블의 하단에 구분선이 있는것같은데 삭제해줘. 컨테이너의 상단에도 구분선이 있어.
## 추가 2 (10-5 · 원문 그대로)
1. 구조물 조합의 ?버튼은 설명같은데 미리보기의 ? 버튼을 미리보기 항목 옆으로 이동하고 설명과 지금처럼 단축키와 기능 설명과 사용법을 작성해줘.
2. 구조물 조합의 파일 불러오기 기능은 미리보기에서 보다는 상세 페이지에서 작업할 내용임. 미리보기에서는 항목 삭제
3. M01의 단가산출 로직의 항목 상세에서 우측 패널에 대한 구현이 잘못됨. 우측 흐름도의 패널과 원문 패널을 별도로 구성하고 사용자가 두개의 패널을 각각 접었다 펼수 있어야함. 단 동시에 두개의 패널을 볼 경우는 없으며 폭정보는 공유하고 있고 둘다 접힌 상태일 수는 있음.
4. 구조물 조합의 단가산출 상세화면에도 원문보기 우측 패널이 있어야함. 우측 패널은 단가산출 로직을 선택할 경우에 원문을 보여주는 기능이며 패널을 접었다 펼수 있어야함.
5. 원문 패널 추가에 따라 현재 단가산출 로직 항목을 선택하면 옵션창이 보이나 선택하면 하이라이트를 하고 원문을 보일수 있게 함. 그리고 옵션설정에 대한 조정은 올리기, 내리기, 빼기 버튼의 이름을 위로, 아래로, 삭제로 변경하고 아래로 와 삭제 사이에 신규 버튼(이름 : 옵션)을 추가하여 선택할 수 있게 변경. 버튼 사이간격에 대해 템플릿화하여 적용.
6. 구조물 조합의 면적산출 우측패널에 별도의 도면 패널 추가(기능은 4번 요청 확인)
@@ -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);
})();