From 2334a2abcf517d07c5f5112a24f0f6435cb716c9 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 13:13:57 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=EF=BB=BFfix(M02):=20=EC=9D=BC=EC=9C=84?= =?UTF-8?q?=EB=8C=80=EA=B0=80=20=ED=91=9C=20=EC=9D=B4=EB=A6=84=20=ED=95=9C?= =?UTF-8?q?=20=EC=A4=84=20=EB=A7=90=EC=A4=84=EC=9E=84=20=C2=B7=20=EC=B9=B8?= =?UTF-8?q?=20=EC=84=A0=20=EC=9D=B4=EC=96=B4=EC=A7=90(45-7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이름 칸 flex 뺌(칸 선 끊김 원인) · 코드 · 이름 · 설명 글마다 한 줄 말줄임 + title - 머리 칸 · 코드 · 규격 줄바꿈 없음 · 전체 합계 칸 테두리를 안쪽 그림자로(칸 선 굵기 그대로) - 확인 시나리오 resources/tester/ui_checks/45-7_일위대가_표_줄.js Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7 --- .../M02_MasterTemplete_Labor.css | 26 ++++- .../M02_MasterTemplete_Labor_Rows.ts | 78 ++++++++------ .../tester/ui_checks/45-7_일위대가_표_줄.js | 100 ++++++++++++++++++ 3 files changed, 167 insertions(+), 37 deletions(-) create mode 100644 resources/tester/ui_checks/45-7_일위대가_표_줄.js 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/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); +})(); From b33ad8fc06ee9cb98f6bf2a7d3044c74cb7fc3eb Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 13:15:49 +0900 Subject: [PATCH 2/3] =?UTF-8?q?chore(sync):=20main.py=20=EB=A5=BC=20dev=20?= =?UTF-8?q?=EC=99=80=20=EA=B0=99=EA=B2=8C=20=E2=80=94=20=EC=8B=9C=EB=86=80?= =?UTF-8?q?=EB=A1=9C=EC=A7=80=EA=B0=80=20=EC=98=AE=EA=B8=B4=20=EA=B0=99?= =?UTF-8?q?=EC=9D=80=20=EB=82=B4=EC=9A=A9(=EB=8D=B0=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=ED=83=91=20PLAN=2046=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EC=98=B5?= =?UTF-8?q?=EC=85=98=20=EA=B8=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FJqWQpoB1RE7hiaQ2wH8qV --- main.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/main.py b/main.py index ed3216512..13dd66cd9 100644 --- a/main.py +++ b/main.py @@ -68,6 +68,8 @@ from M02_MasterTemplete.M02_MasterTemplete_Router_Drawing import router as m02_d from M02_MasterTemplete.M02_MasterTemplete_Router_Items import router as m02_items_router from M02_MasterTemplete.M02_MasterTemplete_Router_Layers import router as m02_layers_router from M02_MasterTemplete.M02_Project_Items_Router import router as m02_project_items_router +from M02_MasterTemplete.StructOptions.M02_SO_Router import read_router as m02_so_read_router +from M02_MasterTemplete.StructOptions.M02_SO_Router import router as m02_so_router from common_util.common_util_audit import note_api_call, record_call_burst from common_util.common_util_auth import ( require_company, @@ -646,6 +648,9 @@ app.include_router( ) # 마스터 템플릿 — 시스템 층은 시스템 관리자만 · 층(회사·개인·프로젝트)·도면 길은 로그인만(권한은 길 안에서). app.include_router(m02_master_template_router, dependencies=system_admin_only) +# 구조물 옵션 보이기 · 그룹(PLAN 46) — 고쳐 씀은 시스템 관리자 · 읽기는 로그인만 +app.include_router(m02_so_router, dependencies=system_admin_only) +app.include_router(m02_so_read_router, dependencies=protected) app.include_router(m02_layers_router, dependencies=protected) app.include_router(m02_items_router, dependencies=protected) app.include_router(m02_combos_router, dependencies=protected) From 66442a5efca34b53c55560b07c321b614fb5958f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 13:30:27 +0900 Subject: [PATCH 3/3] =?UTF-8?q?chore(plan):=2010-5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?2=20=EC=9B=90=EB=AC=B8(=EC=98=A4=EB=A5=B8=EC=AA=BD=20=EC=A0=91?= =?UTF-8?q?=EB=8A=94=20=ED=8C=A8=EB=84=90=20=C2=B7=20=EC=9B=90=EB=AC=B8=20?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=C2=B7=20=EC=A4=84=20=EB=8B=A8=EC=B6=94=20?= =?UTF-8?q?=C2=B7=20=EC=B9=B4=EB=93=9C=20=3F)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FJqWQpoB1RE7hiaQ2wH8qV --- docs/raw/사용자요구/2026-10-05_수정요구.md | 9 +++++++++ 1 file changed, 9 insertions(+) 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번 요청 확인)