From 74c20420c4535bb2ffd96f22bdd09dd0f04b2fbf Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 19:50:28 +0900 Subject: [PATCH 1/6] =?UTF-8?q?=EF=BB=BFfix(M02):=2051-7=20=EC=9D=BC?= =?UTF-8?q?=EC=9C=84=EB=8C=80=EA=B0=80=20=ED=91=9C=20=EC=A4=84=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EC=95=9E=20=E2=96=B8/=E2=96=BE=20=ED=99=94?= =?UTF-8?q?=EC=82=B4=ED=91=9C=20=EC=97=86=EC=95=B0=20=E2=80=94=20=EC=A4=84?= =?UTF-8?q?=20=EB=88=84=EB=A6=84=20=ED=8E=BC=EC=B9=A8=20=C2=B7=20=EA=B0=99?= =?UTF-8?q?=EC=9D=80=20=EC=A4=84=20=EB=8B=A4=EC=8B=9C=20=EB=88=84=EB=A6=84?= =?UTF-8?q?=20=EC=A0=91=ED=9E=98=20=EA=B7=B8=EB=8C=80=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 카드 · 상세 같은 표(logicTable) — 화살표 span · .m02-labrow__fold 스타일 걷어냄 - ui_checks 51-2 시나리오 — 열림 = 고른 줄 수 · 화살표 = 0 기대 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7 --- M02_MasterTemplete/M02_MasterTemplete_Labor.css | 5 ----- M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts | 3 +-- resources/tester/ui_checks/51-2_줄_하위행.js | 6 +++--- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor.css b/M02_MasterTemplete/M02_MasterTemplete_Labor.css index b71f69c92..247bdb005 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor.css @@ -137,11 +137,6 @@ } /* 줄 접고 폄 표(51-2) · 고른 줄 하위 행 — 이름 들여쓰기 · 흐린 바탕 */ -.m02-labrow__fold { - margin-right: var(--spacing-4); - color: var(--color-text-secondary); -} - .m02-labrow__sub td { background: var(--color-bg); font-size: var(--text-caption); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts index 16bb85129..94b8ed902 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts @@ -7,7 +7,7 @@ * 자재목록 = 코드 · 이름 · 규격 · 수량산출 · 단위 · 가격 다섯(기준 칸 강조) · 기준단가 / 반영 금액(43-3). * 금액은 calc 답(없으면 「…」). * 줄 누름 · 줄 단추(위로 · 아래로 · 옵션 · 삭제 · 묶음 `ui-btn-group`)는 `on` 이 있을 때만(보기 갈래 = 없음) · - * 상세는 줄 누름 = 고름(`selected` 줄 하이라이트 · 이름 앞 ▸/▾ · 그 줄 바로 아래 하위 행 · 51-2) · 넣을 값 모달은 [옵션](47-5). + * 상세는 줄 누름 = 고름(`selected` 줄 하이라이트 · 그 줄 바로 아래 하위 행 · 51-2 · 화살표 없음 51-7) · 넣을 값 모달은 [옵션](47-5). * ========================================================================== */ import { createButton, el } from "@ui/ui_template_elements"; @@ -323,7 +323,6 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement el("td", { className: "m02-labrow__name", children: [ - ...(on ? [el("span", { className: "m02-labrow__fold", text: sel ? "▾" : "▸" })] : []), el("span", { className: "m02-labrow__key", text: row.참조?.키 ?? row.로직 }), el("strong", { text: (row.참조 ? line?.이름 : logic?.이름) ?? row.참조?.키 ?? row.로직, diff --git a/resources/tester/ui_checks/51-2_줄_하위행.js b/resources/tester/ui_checks/51-2_줄_하위행.js index 1b20837d9..cb94ba507 100644 --- a/resources/tester/ui_checks/51-2_줄_하위행.js +++ b/resources/tester/ui_checks/51-2_줄_하위행.js @@ -6,7 +6,7 @@ * 기대 수치(카드 · 상세 같음): * - lines[*].subs ≥ 1 · lines[*].cells = head(칸 수 = 머리 칸 수) · lines[*].next = true(고른 줄 바로 아래) * - lines[*].base ≈ lines[*].subBase · lines[*].applied ≈ lines[*].subApplied(반올림 차 ≤ 하위 행 수) - * - lines[*].open = 1 · lines[*].arrow = "▾"(한 줄만 열림) + * - lines[*].open = 1(한 줄만 열림) · lines[*].arrow = 0(이름 앞 화살표 없음 · 51-7) * - again.subs = 0 · again.selected = 0(같은 줄 다시 = 닫힘) · box = 0(따로 상자 없음) */ (async () => { @@ -58,9 +58,9 @@ subBase: Math.round(subs.reduce((a, s) => a + total(s, ".m02-labrow__base"), 0)), applied: total(tr, ".m02-labrow__applied"), subApplied: Math.round(subs.reduce((a, s) => a + total(s, ".m02-labrow__applied"), 0)), - open: [...tb.querySelectorAll(".m02-labrow__fold")].filter((f) => f.innerText === "▾") + open: rowsOf(tb).filter((r) => r.classList.contains("is-selected")).length, + arrow: [...tb.querySelectorAll(".m02-labrow__name")].filter((d) => /[▸▾]/.test(d.innerText)) .length, - arrow: tr.querySelector(".m02-labrow__fold")?.innerText, names: subs.map((s) => s.querySelector(".m02-labrow__subname").innerText).slice(0, 4), }); } From 2cb820a2e88e679b0701c60f8c38f5813b7a8bb8 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 19:52:57 +0900 Subject: [PATCH 2/6] =?UTF-8?q?fix(M02):=2051-7=20=EB=8D=A7=20=EC=9D=BC?= =?UTF-8?q?=EC=9C=84=EB=8C=80=EA=B0=80=20=ED=95=98=EC=9C=84=20=ED=96=89=20?= =?UTF-8?q?=E2=80=94=20=EC=9D=B4=EB=A6=84=20=EA=B8=80=EC=9E=90=20=ED=82=A4?= =?UTF-8?q?=EC=9B=80=20=C2=B7=20=EC=A4=84=20=EC=B4=98=EC=B4=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 하위 행 이름 12 → 14px · 줄 높이 61 → 40px · 칸 여백 12/16 → 4/8px (위 줄 16px · 85px 그대로) - ui_checks 51-2 시나리오 — 하위 행 글자 · 높이 px 전후 적음 · font · h · topH 잼 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7 --- M02_MasterTemplete/M02_MasterTemplete_Labor.css | 9 ++++++--- resources/tester/ui_checks/51-2_줄_하위행.js | 5 +++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor.css b/M02_MasterTemplete/M02_MasterTemplete_Labor.css index 247bdb005..7f212df44 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor.css @@ -136,15 +136,18 @@ color: var(--color-text-secondary); } -/* 줄 접고 폄 표(51-2) · 고른 줄 하위 행 — 이름 들여쓰기 · 흐린 바탕 */ -.m02-labrow__sub td { +/* 줄 접고 폄 표(51-2) · 고른 줄 하위 행 — 이름 들여쓰기 · 흐린 바탕 · 위 줄보다 촘촘(51-7) */ +.m02-labrow .m02-labrow__sub td { + padding: var(--spacing-4) var(--spacing-8); background: var(--color-bg); font-size: var(--text-caption); + line-height: 1.3; } -.m02-labrow__sub .m02-labrow__subname { +.m02-labrow .m02-labrow__sub .m02-labrow__subname { padding-left: var(--spacing-24); color: var(--color-text-secondary); + font-size: var(--text-body-sm); } /* 별칭 칸(44-6) — 칸 안 글상자 · 누르면 바로 고침 */ diff --git a/resources/tester/ui_checks/51-2_줄_하위행.js b/resources/tester/ui_checks/51-2_줄_하위행.js index cb94ba507..0971db5b7 100644 --- a/resources/tester/ui_checks/51-2_줄_하위행.js +++ b/resources/tester/ui_checks/51-2_줄_하위행.js @@ -7,6 +7,8 @@ * - lines[*].subs ≥ 1 · lines[*].cells = head(칸 수 = 머리 칸 수) · lines[*].next = true(고른 줄 바로 아래) * - lines[*].base ≈ lines[*].subBase · lines[*].applied ≈ lines[*].subApplied(반올림 차 ≤ 하위 행 수) * - lines[*].open = 1(한 줄만 열림) · lines[*].arrow = 0(이름 앞 화살표 없음 · 51-7) + * - lines[*].font = 하위 행 이름 글자 px · lines[*].h = 하위 행 높이 px · lines[*].topH = 위 줄 높이 px(51-7 촘촘) + * 전 font 12 · h 61 · 칸 여백 12/16 → 후 font 14 · h 40 · 칸 여백 4/8(위 줄 font 16 · topH 85 그대로) * - again.subs = 0 · again.selected = 0(같은 줄 다시 = 닫힘) · box = 0(따로 상자 없음) */ (async () => { @@ -61,6 +63,9 @@ open: rowsOf(tb).filter((r) => r.classList.contains("is-selected")).length, arrow: [...tb.querySelectorAll(".m02-labrow__name")].filter((d) => /[▸▾]/.test(d.innerText)) .length, + font: parseFloat(getComputedStyle(subs[0].querySelector(".m02-labrow__subname")).fontSize), + h: Math.round(subs[0].getBoundingClientRect().height), + topH: Math.round(tr.getBoundingClientRect().height), names: subs.map((s) => s.querySelector(".m02-labrow__subname").innerText).slice(0, 4), }); } From ae04b403d68ac3399788d79dd5902765f2dc2f39 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 20:01:14 +0900 Subject: [PATCH 3/6] =?UTF-8?q?fix(ui):=2051-8=20=EC=98=A4=EB=A5=B8?= =?UTF-8?q?=EC=AA=BD=20=ED=8C=A8=EB=84=90=20=EB=86=92=EC=9D=B4=20=E2=80=94?= =?UTF-8?q?=20=EC=B9=B8=20=ED=81=AC=EA=B8=B0=EA=B0=80=20=EB=B0=94=EB=80=8C?= =?UTF-8?q?=EB=A9=B4=20=EB=8B=A4=EC=8B=9C=20=EC=9E=BC(M01=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=83=81=EC=84=B8=20240=20=EA=B3=A0=EC=A0=95=20?= =?UTF-8?q?=ED=95=B4=EC=86=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 원인: 패널 높이를 붙은 직후 한 번만 잼 · M01 은 내용이 늦게 차 그때 칸이 짧아 최소 240 · 스크롤·창 크기 바뀜 전까지 그대로 - 고침: 패널 · 부모 칸 크기 지켜봄(ResizeObserver) — 다시 붙음 · 내용 늦게 참에도 다시 잼 - 51-1 확인: 스크롤 알림 없는 「그대로」 줄 추가(알림이 흠을 가림) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK --- resources/tester/ui_checks/51-1_오른쪽패널_높이.js | 11 ++++++++++- ui_template/ui_template_side_panels.ts | 10 ++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/resources/tester/ui_checks/51-1_오른쪽패널_높이.js b/resources/tester/ui_checks/51-1_오른쪽패널_높이.js index ba3fabdd3..1a447c13b 100644 --- a/resources/tester/ui_checks/51-1_오른쪽패널_높이.js +++ b/resources/tester/ui_checks/51-1_오른쪽패널_높이.js @@ -1,7 +1,7 @@ /* 51-1 — 오른쪽 패널(공용 ui_template_side_panels) 높이 = innerHeight − 머리띠 · 스크롤 처음/중간/끝 화면 안 * 시작 화면: 패널이 이미 열린 상세 화면 하나(M02 일위대가 상세 · M02 면적산출 · M01 로직 상세 중 하나) · 화면마다 한 번씩 부름 * 부르는 법: orca eval --expression "$(tr '\n' ' ' < 이 파일)" (async 식 · JSON 글을 돌려줌) - * 기대: ok = true · h = want(innerHeight − max(머리띠, 칸 위)) ±2 · 칸이 올라가 붙은 뒤는 innerHeight − 머리띠 · inView = 처음·중간·끝 모두 true + * 기대: ok = true · h = want(innerHeight − max(머리띠, 칸 위)) ±2 · 칸이 올라가 붙은 뒤는 innerHeight − 머리띠 · inView = 그대로·처음·중간·끝 모두 true */ (async () => { const w = (ms) => new Promise((r) => setTimeout(r, ms)); @@ -38,7 +38,16 @@ }; }; const was = scroller.scrollTop; + // 스크롤 알림 없이 지금 높이 — 내용이 늦게 차도 스스로 다시 쟀는지(알림을 쏘면 가려짐 · 가린 탭은 먼저 orca screenshot 으로 한 장 그림) + const r0 = sp.getBoundingClientRect(); + const still = { + name: "그대로", + h: Math.round(r0.height), + want: Math.round(innerHeight - Math.max(head, sp.parentElement.getBoundingClientRect().top)), + inView: r0.top >= head - 2 && r0.bottom <= innerHeight + 2, + }; const rows = [ + still, await sample("처음", 0), await sample("중간", Math.round(max / 2)), await sample("끝", max), diff --git a/ui_template/ui_template_side_panels.ts b/ui_template/ui_template_side_panels.ts index 2041a5c04..93aa4c17c 100644 --- a/ui_template/ui_template_side_panels.ts +++ b/ui_template/ui_template_side_panels.ts @@ -185,10 +185,18 @@ function scroller(node: HTMLElement): HTMLElement | null { */ function followView(node: HTMLElement): void { const ref = new WeakRef(node); // 떼었다 다시 붙이는 화면(M01)도 있어 떼어져도 듣기는 남김 · 버려지면 그침 + // 다시 붙음 · 칸 내용이 늦게 차 칸 크기가 바뀜(스크롤 없이) → 다시 잼 + const sized = new ResizeObserver(() => place()); + let watched: Element | null = null; const place = (): void => { const root = ref.deref(); if (!root) return stop(); if (!root.isConnected) return; + if (root.parentElement && root.parentElement !== watched) { + if (watched) sized.unobserve(watched); + watched = root.parentElement; + sized.observe(watched); + } const box = scroller(root)?.getBoundingClientRect(); const head = document.querySelector(".app-header")?.getBoundingClientRect().bottom ?? 0; const view = Math.max(head, box?.top ?? 0); @@ -206,8 +214,10 @@ function followView(node: HTMLElement): void { const stop = (): void => { window.removeEventListener("scroll", place, true); window.removeEventListener("resize", place); + sized.disconnect(); }; window.addEventListener("scroll", place, true); window.addEventListener("resize", place); + sized.observe(node); setTimeout(place); // 붙은 뒤 한 번 } From 1ea098a119dc820e86f4b570c92423dd7a855454 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 20:03:22 +0900 Subject: [PATCH 4/6] =?UTF-8?q?chore(M02):=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20=ED=8E=B8=EC=A7=91=20=E2=80=94=20=EB=A7=88=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=20=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20item3=20=C2=B7=2010-5?= =?UTF-8?q?=20=EC=9A=94=EA=B5=AC=20=EC=9B=90=EB=AC=B8=20=EB=8D=A7?= 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 | 6 + resources/master_template/labor/item3.json | 155 ++++++++++++++++++++- 2 files changed, 159 insertions(+), 2 deletions(-) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index 50ae3392e..1499240a3 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -104,3 +104,9 @@ 단위를 선택하게 하는 항목들 중 선택 후 나오는 단위 표기 버튼이 너무 작음 저장같은 버튼과 같은 크기로 변경 사전조합에 상세에서 보여지는 단가산출 로직의 선택 기능이 없음 + +구조물 조합의 일위대가에서 조합 테이블에 넣어준 상세 항목 펼쳐짐 화살표는 필요없어. + +그리고 로직 고르기 모달창에서 구분과 상세 구분은 2열 구조로 변경하고 찾을때 나온 항목들의 이름들은 좌측 맞춤으로 글자를 맞춰야지. 찾기의 공용화 양식이 없는거야?템플릿 있을것같은데? + +단가산출과 일위대가 사전조합에서 테이블의 양식도 M01에 공통테이블 양식이 있을것같고 버튼들도 M01에 템플릿이 있을거야. 확인해봐 그리고 사전조합의 경우에 검색은 별도의 창이 아니라 구조물 조합의 일위대가 테이블의 조건 추가와 같은 기능이어야. diff --git a/resources/master_template/labor/item3.json b/resources/master_template/labor/item3.json index b512db52a..964bcab3f 100644 --- a/resources/master_template/labor/item3.json +++ b/resources/master_template/labor/item3.json @@ -11,21 +11,44 @@ "담은로직": [ { "로직": "GF000219", + "별칭": null, "메모": "메쌓기 — 돌쌓기 인력", "입력값": {}, "수량": null }, { "로직": "GF000119", + "별칭": null, "메모": "노체포설 — 굴착기 0.7㎥", "입력값": {}, "수량": null }, { "로직": "GF000137", + "별칭": null, "메모": "운반 — 중기운반 트레일러(20ton)", "입력값": {}, "수량": null + }, + { + "참조": { + "층": "system", + "주인": null, + "키": "SP-000001" + }, + "별칭": null, + "메모": null, + "수량": null + }, + { + "참조": { + "층": "system", + "주인": null, + "키": "SC-000001" + }, + "별칭": null, + "메모": null, + "수량": null } ], "자재": [], @@ -43,6 +66,28 @@ "경비": 0, "계": 79279.668 }, + "줄": [ + { + "이름": "석공", + "단위": "인", + "수량": 0.195, + "단가": 268908, + "금액": 52437.06, + "비목": { + "노무비": 52437.06 + } + }, + { + "이름": "보통인부", + "단위": "인", + "수량": 0.156, + "단가": 172068, + "금액": 26842.608, + "비목": { + "노무비": 26842.608 + } + } + ], "까닭": null, "수량": null, "수량출처": "없음", @@ -59,6 +104,20 @@ "경비": 1012.3273551844981, "계": 4522.956900158043 }, + "줄": [ + { + "이름": "굴착기(무한궤도) 0.7㎥", + "단위": "시간", + "수량": 0.04122861265718408, + "단가": 109704.31961333334, + "금액": 4522.956900158043, + "비목": { + "노무비": 2433.321308321308, + "재료비": 1077.3082366522367, + "경비": 1012.3273551844981 + } + } + ], "까닭": null, "수량": null, "수량출처": "없음", @@ -75,18 +134,110 @@ "경비": 13280.479259259258, "계": 83943.98719017902 }, + "줄": [ + { + "이름": "트럭 트랙터 및 평판트레일러 20ton", + "단위": "시간", + "수량": 0.7670370370370371, + "단가": 109439.28798333333, + "금액": 83943.98719017902, + "비목": { + "노무비": 38181.82530864197, + "재료비": 32481.682622277778, + "경비": 13280.479259259258 + } + }, + { + "이름": "트럭 10.5ton", + "단위": "시간", + "수량": 0, + "단가": 97740.38995333333, + "금액": 0, + "비목": { + "노무비": 0, + "재료비": 0, + "경비": 0 + } + } + ], "까닭": null, "수량": null, "수량출처": "없음", "반영": null + }, + { + "참조": { + "층": "system", + "주인": null, + "키": "SP-000001" + }, + "이름": "돌쌓기 메쌓기(인력)", + "알약": "참조|system||SP-000001", + "분모": "원/㎡", + "배": 1, + "기준": { + "노무비": 79279.668, + "재료비": 0, + "경비": 0, + "계": 79279.668 + }, + "까닭": null, + "원로직": "GF000219", + "줄": [ + { + "이름": "석공", + "단위": "인", + "수량": 0.195, + "단가": 268908, + "금액": 52437.06, + "비목": { + "노무비": 52437.06 + } + }, + { + "이름": "보통인부", + "단위": "인", + "수량": 0.156, + "단가": 172068, + "금액": 26842.608, + "비목": { + "노무비": 26842.608 + } + } + ], + "수량": null, + "수량출처": "없음", + "반영": null + }, + { + "참조": { + "층": "system", + "주인": null, + "키": "SC-000001" + }, + "이름": "테스트 사전조합", + "알약": "참조|system||SC-000001", + "분모": null, + "배": 1, + "기준": { + "노무비": 0, + "재료비": 0, + "경비": 0, + "계": 0 + }, + "까닭": null, + "안": [], + "수량": null, + "수량출처": "없음", + "반영": null } ], "자재": [], "기준합": { - "노무비": 119894.81461696328, + "노무비": 199174.48261696327, "재료비": 33558.99085893002, "경비": 14292.806614443756, - "계": 167746.61209033706 + "계": 247026.28009033707 }, "반영합": { "노무비": 0, From 9443a0371f0b27fa3c3b519642dcb12ba688dc83 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 20:05:32 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat(M02):=20=EC=84=9C=EA=B3=A0=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=ED=91=9C=20M01=20=ED=91=9C=20=EC=96=91=EC=8B=9D=20?= =?UTF-8?q?=C2=B7=20=EC=82=AC=EC=A0=84=EC=A1=B0=ED=95=A9=20[=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80]=20=EA=B0=99=EC=9D=80=20?= =?UTF-8?q?=EA=B3=A0=EB=A5=B4=EA=B8=B0=20=EC=B0=BD(51-10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 서고 목록표(단가산출 승격 · 일위대가 사전조합) = M01 단가산출 로직 목록 양식 — 찾기 줄(buildHeadFilters) · 쪽(buildPager · 30줄) · 표 m01-master__grid · 줄 m01-master__table-row 를 M01 에서 import - 머리 단추 꼴 = M01 [신규 로직] · [로직 복사/생성] · [로직 지우기] 와 같음(filled · ghost · danger 빨강) · 단추 틈 8px(m01-head 와 같음) - 사전조합 상세 [추가] 따로 단추 없앰 — 일위대가 상세와 같은 [로직 추가] · 같은 고르기 창(Labor_Pick · only = 서고 줄만) - 시험 기대 글 고침(card_shell 늘 참이던 assert) · 화면 시나리오 51-10 · 51-6 단추 이름 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- .../M02_MasterTemplete_Labor_Detail.ts | 32 ++-- .../M02_MasterTemplete_Labor_Text.ts | 1 - .../M02_MasterTemplete_Library_List.ts | 32 +++- .../M02_MasterTemplete_UI_Style.css | 13 +- resources/tester/test_m02_card_shell.py | 6 +- .../tester/ui_checks/51-10_서고_표양식.js | 149 ++++++++++++++++++ .../tester/ui_checks/51-6_사전조합_줄넣기.js | 6 +- 7 files changed, 198 insertions(+), 41 deletions(-) create mode 100644 resources/tester/ui_checks/51-10_서고_표양식.js diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index b9974542c..441a2a223 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -9,7 +9,7 @@ * · 같은 로직 두 번 · 같은 자재 두 번 막음 · 빈 일위대가도 저장 됨 * · 오른쪽 「원문」 접는 패널(47-4 · 상세만 · 공용 `ui_template_side_panels`) = 고른 줄 로직의 출처 원문 * · 서고 모드(50-3 · `mode`) = 단가승격 · 사전조합 — 이름 칸 · 단가산출 로직 표 · 원문만(자재 · 면적 · 다이어그램 없음) · - * 승격은 로직 한 줄(입력값 고정 · 더하기 · 순서 · 수량 없음) · 사전조합 = [추가] · 수량 손 입력(기본 1) · 순서 · 삭제 + * 승격은 로직 한 줄(입력값 고정 · 더하기 · 순서 · 수량 없음) · 사전조합 = [로직 추가](같은 고르기 창 · 서고 줄만) · 수량 손 입력(기본 1) · 순서 · 삭제 * ========================================================================== */ import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements"; @@ -378,22 +378,20 @@ export function mountLaborDetail( changed(); }, }; - /** 사전조합 [추가] — 서고 줄만 고르기(위 층만 · 자기 자신 · 순환은 [저장] 때 서버 422) */ - const addRef = createButton({ - label: tc("Lib_AddRow"), - variant: "filled", - onClick: () => openLogicPicker(() => undefined, { ...library, only: true }), - }); const addLogic = createButton({ label: tc("AddLogic"), variant: "filled", onClick: () => - openLogicPicker((x) => { - if (cur.담은로직.some((r) => r.로직 === x.키)) - return showToast(tc("Labor_Already").replace("{value}", `${x.키} ${x.이름}`), "error"); - cur.담은로직.push({ 로직: x.키, 별칭: null, 메모: null, 입력값: {}, 수량: null }); - void loadLogics().then(() => changed()); - }, library), + openLogicPicker( + (x) => { + if (cur.담은로직.some((r) => r.로직 === x.키)) + return showToast(tc("Labor_Already").replace("{value}", `${x.키} ${x.이름}`), "error"); + cur.담은로직.push({ 로직: x.키, 별칭: null, 메모: null, 입력값: {}, 수량: null }); + void loadLogics().then(() => changed()); + // 사전조합 = 같은 창 · 서고 줄만(위 층만 · 자기 자신 · 순환은 [저장] 때 서버 422) + }, + lib ? { ...library, only: true } : library, + ), }); const addMat = createButton({ label: tc("Labor_AddMat"), @@ -478,12 +476,8 @@ export function mountLaborDetail( ], }), ]), - // 승격 = 로직 한 줄(새로 만들 때 고름) · 사전조합 = [추가] 서고 줄만 고르기(51-6) - section( - tc("Logics_Title"), - opt.card || promote ? null : lib ? addRef : addLogic, - logicHost, - ), + // 승격 = 로직 한 줄(새로 만들 때 고름) · 사전조합 = 같은 [로직 추가] · 서고 줄만(51-10) + section(tc("Logics_Title"), opt.card || promote ? null : addLogic, logicHost), ...(lib ? [] : [section(tc("Labor_Mats"), opt.card ? null : addMat, matHost)]), ...(opt.card || lib ? [] : [dgBox]), ], diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts index cff2e9cda..c7be730db 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts @@ -124,7 +124,6 @@ const TEXT = { Lib_Unit: ["단위", "Unit"], Lib_Base: ["기준 금액", "Base amount"], Lib_Date: ["고친 날", "Modified"], - Lib_AddRow: ["추가", "Add"], } as const satisfies Record; export type ComboTextKey = keyof typeof TEXT; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Library_List.ts b/M02_MasterTemplete/M02_MasterTemplete_Library_List.ts index 0b15d996f..3a183094e 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Library_List.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Library_List.ts @@ -3,6 +3,7 @@ * 일위대가 조합 서고 목록표(51-4) — 좌측 항목(단가산출 승격 · 일위대가 사전조합)을 누르면 상세 칸에 뜸. * 표 = 고름 · 키 · 이름 · 단위 · 기준 금액(서버 1 단위 계) · 고친 날 · 줄 누름 = 그 문서 상세. * 머리 [생성] · [복사](고른 줄) · [삭제](고른 줄 · 쓰는 곳 있으면 서버 409 까닭 그대로). + * 꼴 = M01 단가산출 로직 목록 — 찾기 줄(`buildHeadFilters`) · 쪽(`buildPager`) · 표(`m01-master__grid`) 를 M01 에서 import. * ========================================================================== */ import { @@ -14,6 +15,9 @@ import { } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { openModal } from "@ui/ui_template_modal"; +import { buildPager } from "../M01_MasterData/M01_MasterData_UI_Cells"; +import { buildHeadFilters } from "../M01_MasterData/M01_MasterData_UI_Head"; +import "../M01_MasterData/M01_MasterData_UI_Style.css"; import { deleteLayerTemplate, deleteTemplate, @@ -41,6 +45,7 @@ export interface LibraryListOptions { open: (key: string, label: string) => void; } +const SIZE = 30; const why = (error: unknown): string => (error instanceof Error ? error.message : ""); const money = (v: number | null | undefined): string => v == null ? "" : v.toLocaleString("ko-KR", { maximumFractionDigits: 2 }); @@ -54,7 +59,10 @@ export function mountLibraryList( ): { getDoc: () => null; destroy: () => void } { let rows: Row[] = []; let picked: string | null = null; + let page = 1; + const searchField = createInputField({ type: "search", placeholder: tc("Search") }); const body = el("tbody"); + let pager = buildPager(0, SIZE, 1, () => {}); const btnNew = createButton({ label: tc("Lib_Create"), variant: "filled" }); const btnCopy = createButton({ label: tc("Lib_Copy"), variant: "ghost" }); const btnDel = createButton({ label: tc("Lib_Delete"), variant: "danger" }); @@ -63,9 +71,18 @@ export function mountLibraryList( }; const draw = (): void => { + const q = searchField.input.value.trim().toLowerCase(); + const shown = rows.filter((r) => !q || libraryLabel(r).toLowerCase().includes(q)); + page = Math.min(page, Math.max(1, Math.ceil(shown.length / SIZE))); + const next = buildPager(shown.length, SIZE, page, (p) => { + page = p; + draw(); + }); + pager.replaceWith(next); + pager = next; body.replaceChildren( - ...(rows.length - ? rows.map((row) => { + ...(shown.length + ? shown.slice((page - 1) * SIZE, page * SIZE).map((row) => { const radio = el("input", { attrs: { type: "radio", name: `m02-lib-${opt.kind}`, "aria-label": row.이름 }, }) as HTMLInputElement; @@ -78,7 +95,7 @@ export function mountLibraryList( paintButtons(); }); const tr = el("tr", { - className: "m02-lib__row", + className: "m01-master__table-row", attrs: { "data-lib-key": row.이름 }, children: [ pick, @@ -200,14 +217,19 @@ export function mountLibraryList( } }); + searchField.input.addEventListener("input", () => { + page = 1; + draw(); + }); const heads = ["", "Lib_Key", "Lib_Name", "Lib_Unit", "Lib_Base", "Lib_Date"] as const; host.replaceChildren( + buildHeadFilters([searchField.root, pager]), el("div", { - className: "ui-general-block__table-wrap ui-general-block__table-wrap--sticky-head", + className: "m01-master__grid-wrap", attrs: { "data-lib-list": opt.kind }, children: [ el("table", { - className: "ui-general-block__table m02-lib__table", + className: "m01-master__grid", children: [ el("thead", { children: [ diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css index 09567f32b..5fe85ec07 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css @@ -56,16 +56,8 @@ gap: var(--spacing-4); } -/* 일위대가 조합 서고 목록표(51-4) — 줄 누름 = 상세 · 첫 칸 = 고름 */ -.m02-lib__row { - cursor: pointer; -} - -.m02-lib__row:hover { - background: var(--color-bg-hover, rgba(0, 0, 0, 0.04)); -} - -.m02-lib__table .m02-lib__pick { +/* 일위대가 조합 서고 목록표(51-4) — 표 꼴은 M01 목록(`m01-master__grid`) · 첫 칸 = 고름 */ +.m01-master__grid .m02-lib__pick { width: 2.5rem; text-align: center; } @@ -123,6 +115,7 @@ /* 제목 줄 오른쪽 끝 도구(일위대가 「새 조합」 · PLAN 30-5) */ .m02-main__tools { display: inline-flex; + gap: var(--spacing-8); /* M01 머리 줄(`m01-head`)과 같은 틈 */ margin-left: auto; } diff --git a/resources/tester/test_m02_card_shell.py b/resources/tester/test_m02_card_shell.py index 3ffaa76b1..178871a91 100644 --- a/resources/tester/test_m02_card_shell.py +++ b/resources/tester/test_m02_card_shell.py @@ -80,9 +80,9 @@ def test_일위대가_카드는_상세_카드_갈래(): detail = _text("M02_MasterTemplete_Labor_Detail.ts") assert "...(opt.card ? {} : { move })" in detail # 순서 단추 없음 · 빼기는 그대로 # 서고 모드(50-3 · lib)도 더하기 · 다이어그램 없음 - assert ( - "opt.card || promote ? null : lib ? addRef : addLogic" - ) # 51-6 사전조합 = 줄 고르기 in detail + # 51-10 사전조합 = 같은 [로직 추가] · 서고 줄만 + assert "opt.card || promote ? null : addLogic" in detail + assert "lib ? { ...library, only: true } : library" in detail assert "...(opt.card || lib ? [] : [dgBox])," in detail assert "opt.card ? null : addMat" in detail # 49-6 — 카드엔 [자재 추가] 없음 · 상세는 그대로 rows = _text("M02_MasterTemplete_Labor_Rows.ts") diff --git a/resources/tester/ui_checks/51-10_서고_표양식.js b/resources/tester/ui_checks/51-10_서고_표양식.js new file mode 100644 index 000000000..0b472b6ef --- /dev/null +++ b/resources/tester/ui_checks/51-10_서고_표양식.js @@ -0,0 +1,149 @@ +/* 51-10 — 서고 목록표(단가산출 승격 · 일위대가 사전조합) = M01 단가산출 로직 목록 표 양식 · 단추 꼴 · + * 사전조합 상세 [로직 추가] = 일위대가 상세 [로직 추가] 와 같은 고르기 창(서고 줄만) + * 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=일위대가조합&root=master) · 시스템 관리자 + * 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌) + * 쓰기 없음 — 서고 목록 · 문서 읽기는 가짜 응답(fetch 바꿔 끼움 · 끝에 되돌림) + * 기대 수치: + * - table.wrap = "m01-master__grid-wrap" · table.table = "m01-master__grid" · table.row = "m01-master__table-row" + * - table.filters = true(찾기 칸 + 쪽 `m01-master__pager`) · table.sameCell = true(M01 표 칸과 padding · 글자 · 밑줄 같음) + * - table.search = 1(「값 없음」 치면 한 줄) + * - buttons.variants = ["filled", "ghost", "danger"](M01 [신규 로직] · [로직 복사/생성] · [로직 지우기] 와 같음) · buttons.redDelete = true + * - precombo.add = "로직 추가" · picker.label = "로직 고르기" · picker.kinds ⊂ {단가승격, 일위조합} · picker.logicRows = 0 + */ +(async () => { + const wait = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 4000) => { + for (let t = 0; t < ms; t += 100) { + const v = fn(); + if (v) return v; + await wait(100); + } + return null; + }; + const SYS = { 층: "system", 주인: null }; + const rows = [ + { + 종류: "promote", + 이름: "SP-999901", + 판: "a", + 수정일: "2026-10-05", + 제목: "시험 승격", + 단위: "㎡", + 기준금액: 1234.5, + }, + { + 종류: "promote", + 이름: "SP-999902", + 판: "b", + 수정일: "2026-10-05", + 제목: "값 없음", + 단위: null, + 기준금액: null, + }, + { + 종류: "precombo", + 이름: "SC-999901", + 판: "c", + 수정일: "2026-10-05", + 제목: "시험 묶음", + 단위: "개소", + 기준금액: 10, + }, + ]; + const doc = { + 종류: "사전조합", + 키: "SC-999901", + 이름: "시험 묶음", + 단위: "개소", + 담은로직: [{ 참조: { ...SYS, 키: "SP-999901" }, 수량: { 손: 1 } }], + }; + const json = (body, status = 200) => + new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } }); + const real = window.fetch; + window.fetch = async (input, init = {}) => { + const url = String(input?.url ?? input); + const method = (init.method ?? "GET").toUpperCase(); + if (method === "GET" && /\/m02\/layers\/system\/templates(\?|$)/.test(url)) { + const got = await (await real(input, init)).json(); + return json({ ...got, 양식: [...got.양식.filter((r) => !/^S[PC]-/.test(r.이름)), ...rows] }); + } + if (method === "GET" && url.includes("/templates/precombo/SC-999901")) + return json({ 종류: "precombo", 이름: "SC-999901", 판: "c", 문서: doc }); + if (method !== "GET" && !url.includes("/calc")) + return json({ detail: "시험 — 쓰기 막음" }, 400); + return real(input, init); + }; + const sec = [...document.querySelectorAll(".m02-side__lists > section")].find( + (s) => s.querySelector(".ui-collapsible__title")?.textContent?.trim() === "일위대가 조합", + ); + const items = [...sec.querySelectorAll("[data-precombo]")]; + const list = (kind) => document.querySelector(`[data-lib-list="${kind}"]`); + const tools = () => [...document.querySelectorAll(".m02-main__slot .m02-main__tools button")]; + const box = (n) => { + const s = getComputedStyle(n); + return [s.paddingTop, s.paddingLeft, s.fontSize, s.borderBottomWidth].join("|"); + }; + try { + items[0].click(); + await until(() => list("promote")?.querySelector(`[data-lib-key="SP-999902"]`)); + const wrap = list("promote"); + const td = wrap.querySelector("[data-lib-key] td:nth-child(3)"); + // M01 목록 표와 같은 클래스로 짠 견본 칸 — 같은 CSS 면 같은 칸 꼴 + const ref = document.createElement("div"); + ref.className = "m01-master__grid-wrap"; + ref.innerHTML = + '
시험 승격
'; + wrap.parentElement.append(ref); + const table = { + wrap: wrap.className, + table: wrap.querySelector("table").className, + row: wrap.querySelector("[data-lib-key]").className, + filters: + !!wrap.parentElement.querySelector(".m01-head__filters input[type=search]") && + !!wrap.parentElement.querySelector(".m01-head__filters .m01-master__pager"), + sameCell: box(td) === box(ref.querySelector("td")), + cell: box(td), + }; + ref.remove(); + const search = wrap.parentElement.querySelector(".m01-head__filters input[type=search]"); + search.value = "값 없음"; + search.dispatchEvent(new Event("input")); + await wait(100); + table.search = list("promote").querySelectorAll("[data-lib-key]").length; + search.value = ""; + search.dispatchEvent(new Event("input")); + const v = (b) => ["filled", "ghost", "danger"].find((x) => b.className.includes(`--${x}`)); + const buttons = { + labels: tools().map((b) => b.textContent), + variants: tools().map(v), + redDelete: /rgb\((1\d\d|2\d\d), (\d|[1-9]\d), (\d|[1-9]\d)\)/.test( + getComputedStyle(tools()[2]).backgroundColor + getComputedStyle(tools()[2]).color, + ), + deleteColor: getComputedStyle(tools()[2]).backgroundColor, + }; + // 사전조합 상세 [로직 추가] + items[1].click(); + const row = await until(() => + document.querySelector('[data-lib-key="SC-999901"] td:nth-child(3)'), + ); + row.click(); + const edit = await until(() => document.querySelector(".m02-main__host [data-labor]")); + await wait(300); + const add = [...edit.querySelectorAll("button")].find((b) => b.textContent === "로직 추가"); + const precombo = { add: add?.textContent ?? null }; + add?.click(); + const pick = await until(() => document.querySelector(".m01-logic__pick")); + await until(() => pick?.querySelectorAll("[data-pick]").length, 3000); + const kinds = [...(pick?.querySelectorAll("[data-pick-kind]") ?? [])].map((x) => x.textContent); + const picker = { + label: pick?.getAttribute("aria-label") ?? null, + kinds: [...new Set(kinds)], + logicRows: kinds.filter((k) => k === "단가산출").length, + rows: kinds.length, + }; + pick?.closest(".m01-logic__backdrop")?.remove(); + return JSON.stringify({ table, buttons, precombo, picker }); + } finally { + window.fetch = real; + } +})(); diff --git a/resources/tester/ui_checks/51-6_사전조합_줄넣기.js b/resources/tester/ui_checks/51-6_사전조합_줄넣기.js index 213b08cc8..689cf8434 100644 --- a/resources/tester/ui_checks/51-6_사전조합_줄넣기.js +++ b/resources/tester/ui_checks/51-6_사전조합_줄넣기.js @@ -115,7 +115,7 @@ await wait(300); }; const pickerRow = async (key) => { - btn(edit(), "추가").click(); + btn(edit(), "로직 추가").click(); const row = await until(() => document.querySelector(`.m01-logic__pick [data-pick="${key}"]`)); return row; }; @@ -123,13 +123,13 @@ // 단가승격 상세 — 한 줄 고정 await openDoc(0, "SP-999901"); const promote = { - add: !!btn(edit(), "추가"), + add: !!btn(edit(), "로직 추가"), rows: lines().length, move: edit().querySelectorAll(".m02-labrow__move button").length, }; // 사전조합 상세 await openDoc(1, "SC-999901"); - const precombo = { add: !!btn(edit(), "추가"), rows: lines().length }; + const precombo = { add: !!btn(edit(), "로직 추가"), rows: lines().length }; const first = await pickerRow("SP-999902"); const pick = document.querySelector(".m01-logic__pick"); const picker = { From da1fe40202341f93bddcb9e155a73ae0b5a93a7a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 20:08:39 +0900 Subject: [PATCH 6/6] =?UTF-8?q?test(M02):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=ED=8C=A8=EB=84=90=20=EC=8B=9C=ED=97=98=20Kind=20=EA=B8=80=20?= =?UTF-8?q?=E2=80=94=2050-3=20=EC=84=9C=EA=B3=A0=20=EC=A2=85=EB=A5=98(prom?= =?UTF-8?q?ote=20=C2=B7=20precombo)=20=EB=8D=94=ED=95=9C=20=EA=BC=B4?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EC=B6=A4?= 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 --- resources/tester/test_m02_structure_panel.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/resources/tester/test_m02_structure_panel.py b/resources/tester/test_m02_structure_panel.py index 323a0f461..f2771090c 100644 --- a/resources/tester/test_m02_structure_panel.py +++ b/resources/tester/test_m02_structure_panel.py @@ -77,7 +77,10 @@ def test_sub2_가_부르는_이름과_모양(): r"interface StructureNumbers \{\s*도번: string;\s*면적번: string;\s*/\*\* 일위대가[^*]*\*/\s*일위대가번: string;\s*\}", body, ) - assert re.search(r'export type Kind = [^;]*"area" \| "labor";', body) # 재료산출 없앰 + assert re.search(r'export type Kind = [^;]*"area" \| "labor"', body) # 재료산출 없앰 + assert '"material"' not in re.search(r"export type Kind = [^;]*;", body).group( + 0 + ) # 50-3 서고 종류 더함 def test_집계표만_컨테이너를_붙임():