From ccc2fe080fd28a3835dcceae7ccad899d5395a95 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 1 Nov 2026 15:04:41 +0900 Subject: [PATCH] =?UTF-8?q?feat(716-3):=20M02=20=EC=99=BC=EC=AA=BD=20?= =?UTF-8?q?=EC=B9=B8=20=EC=B0=A8=EB=A1=80=20=C2=B7=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=C2=B7=20=EA=B5=AC=EB=B6=84=EC=84=A0=20=E2=80=94=20=EC=9D=BC?= =?UTF-8?q?=EC=9C=84=EB=8C=80=EA=B0=80=20=E2=80=BA=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EB=AC=BC=20=EC=A1=B0=ED=95=A9=20=E2=80=BA=20=EA=B5=AC=EB=B6=84?= =?UTF-8?q?=EC=84=A0=20=E2=80=BA=20=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0=20?= =?UTF-8?q?=C2=B7=20=EC=98=B5=EC=85=98=20=EC=A4=84=20=EB=BA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 일위대가(옛 사전조합) 제목 locale 키 M02_PrecomboTitle · 나무 안 글 · 마우스 오른쪽 메뉴 · 빈 글을 일위대가로 - 구조물 옵션 정의 읽기 줄 뺌(M01 에 입구 있음) · open=옵션 은 M01 로 그대로 돌려보냄 - 시험: PRECOMBO_TITLE 상수 기대 → locale 키 기대로 고침 · 칸 차례 시험 더함 Co-Authored-By: Claude Sonnet 5.5 --- .../M02_MasterTemplete_Labor_Text.ts | 12 ++-- .../M02_MasterTemplete_UI_Side.ts | 63 +++++++------------ .../M02_MasterTemplete_UI_Side_Precombo.ts | 11 ++-- resources/tester/test_m02_precombo_scope.py | 19 +++++- ui_template/ui_template_locale_m2.ts | 1 + 5 files changed, 54 insertions(+), 52 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts index d64dbdfc9..3e898c675 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts @@ -101,7 +101,7 @@ const TEXT = { Labor_LineApplied: ["반영(수량 곱함)", "Applied (× quantity)"], Labor_LineBase: ["기준", "Base"], Labor_FromLogic: ["원 로직", "Source logic"], - Lib_Precombo: ["사전조합", "Pre-combination"], + Lib_Precombo: ["일위대가", "Unit-cost item"], Lib_System: ["마스터", "Master"], Lib_Company: ["회사", "Company"], Lib_Personal: ["개인", "Personal"], @@ -156,13 +156,13 @@ const TEXT = { In_Close: ["닫기", "Close"], Lib_Name: ["이름", "Name"], // 서고 목록표(51-4) - Lib_PrecomboAll: ["일위대가 사전조합", "Pre-combination"], - Lib_None: ["사전조합 없음", "No pre-combination"], + Lib_PrecomboAll: ["일위대가", "Unit-cost items"], + Lib_None: ["일위대가 없음", "No unit-cost items"], Lib_Unsorted: ["미분류", "Unsorted"], - Lib_NewHere: ["여기에 새 사전조합", "New pre-combination here"], + Lib_NewHere: ["여기에 새 일위대가", "New unit-cost item here"], Lib_FolderFull: [ - "폴더 안에 사전조합 {n}개 — 먼저 옮기거나 지운 뒤 지움", - "Folder holds {n} pre-combination(s) — move or delete them first", + "폴더 안에 일위대가 {n}개 — 먼저 옮기거나 지운 뒤 지움", + "Folder holds {n} unit-cost item(s) — move or delete them first", ], Lib_FolderDelete: ["「{value}」 빈 폴더를 지움", "Delete empty folder “{value}”"], Lib_Create: ["생성", "Create"], diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts index b6b5fc63e..7f1d5b88d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts @@ -1,8 +1,9 @@ /* ============================================================================= * M02_MasterTemplete_UI_Side.ts - * 마스터 템플릿 좌측 패널 — 「구조물 조합」(층 단추 · 나무) · 「일위대가 사전조합」(층 단추 따로 · 2단 폴더 나무 · - * `UI_Side_Precombo` · 50-3 · 134-3) · 「일위대가 실무자료」(구조물 조합 층 · `Hopyo_Side` · 324-4) · - * 「도면 양식」(새로 · 본떠 · 지우기). + * 마스터 템플릿 좌측 패널 차례(716-3) — 「일위대가」(층 단추 따로 · 2단 폴더 나무 · `UI_Side_Precombo` · + * 50-3 · 134-3) › 「구조물 조합」(층 단추 · 나무) › 수평 구분선 › 「일위대가 불러오기」(구조물 조합 층 · + * `Hopyo_Side` · 324-4) › 「도면 양식」(새로 · 본떠 · 지우기) › 실무문서. 「단가산출」 칸은 맨 위(717-2a). + * 구조물 옵션 정의는 M01 로 옮김(716-1). * 서버 길은 계약 `6_계약.md`. * 구조물 도면 · 일위대가 · 면적산출 상세는 좌측 줄이 아니라 항목 카드 [수정]으로(PLAN 45-2). * 화면 이름 「구조물 조합」(PLAN 26-7a) — 파일 이름 · 양식 키 · API 길은 「구조물집계표」 그대로. @@ -30,17 +31,13 @@ import { type TemplateInfo, } from "./M02_MasterTemplete_Api_Fetch"; import { STRUCTURE_TABLE } from "./M02_MasterTemplete_Structure"; -import { buildOptionsSection } from "./StructOptions/M02_SO_Options"; import { buildPracticeSection } from "./M02_MasterTemplete_Practice"; import { HOPYO_BUILT } from "./M02_MasterTemplete_Hopyo_Check"; import { buildHopyoSection } from "./M02_MasterTemplete_Hopyo_Side"; import { canEdit, type Selection } from "./M02_MasterTemplete_UI_Main"; -import { - buildPrecomboSection, - PRECOMBO_TITLE, - type PrecomboOptions, -} from "./M02_MasterTemplete_UI_Side_Precombo"; +import { buildPrecomboSection, type PrecomboOptions } from "./M02_MasterTemplete_UI_Side_Precombo"; import { createEmpty } from "@ui/ui_template_empty"; +import { createDivider } from "@ui/ui_template_divider"; const KINDS: { kind: Kind; label: () => string }[] = [ { kind: "table", label: () => L("M02_KindTable") }, @@ -68,7 +65,7 @@ export interface SideOptions { showView: (label: string, node: HTMLElement) => (dirty: boolean) => void; /** 「구조물 조합」 줄 아래 — 층 단추 · 고른 층의 구조물 라이브러리 나무(PLAN 29-2 · 페이지가 만듦) */ tree?: HTMLElement[]; - /** 「일위대가 사전조합」 층(134-3 · 페이지 층과 따로 · 층 단추는 페이지가 만듦) */ + /** 「일위대가」 층(134-3 · 페이지 층과 따로 · 층 단추는 페이지가 만듦) */ precombo: Pick; } @@ -77,7 +74,7 @@ export interface SideHandle { refresh: () => Promise; /** 고름 표시를 지금 열린 것 한 줄로 — 편집 칸이 무엇을 열든(카드 [도면 수정] 등) 부름 */ paint: () => void; - /** 진입 해시 `open`(PLAN 27-1d) — 그 컨테이너를 펼침(옵션은 열기까지) · 옵션 · 도면은 시스템 층에만 */ + /** 진입 해시 `open`(PLAN 27-1d) — 그 컨테이너를 펼침 · 도면은 시스템 층에만 · 옵션은 M01 로 옮겨 아무 일 없음 */ reveal: (target: "옵션" | "도면" | "일위대가조합") => void; /** 사전조합 층이 바뀜(134-3) — 그 컨테이너만 다시 그림 */ refreshPrecombo: () => Promise; @@ -120,13 +117,6 @@ export function buildSide(opt: SideOptions): SideHandle { del.addEventListener("click", () => void deleteCurrent()); return [el("div", { className: "m02-side__row", children: [btnNew, copy, del] })]; }; - // 구조물 옵션 보이기 — 구조물 집계 양식 아래 별도 줄 하나(누르면 메인 칸에 종류별 옵션 체크) - const options = buildOptionsSection({ - isAdmin: canEdit(layer, opt.isAdmin), - confirmLeave: opt.confirmLeave, - show: opt.showView, - onOpen: () => (practice?.clear(), paint()), - }); /** 컨테이너 제목을 펼치면 그 화면을 엶 — 같은 이름 줄 단추를 갈음(PLAN 29-6) */ const openOnExpand = (sec: HTMLElement, run: () => void): HTMLElement => { sec.querySelector(".ui-collapsible__title")?.addEventListener("click", () => { @@ -143,27 +133,24 @@ export function buildSide(opt: SideOptions): SideHandle { openOnExpand(summary, async () => { if (!isPicked("table", STRUCTURE_TABLE) && (await opt.confirmLeave())) opt.onOpen(null); }); - // 일위대가 사전조합 서고(50-3 · 134-3) — 구조물 조합 바로 아래 · 층은 페이지 층과 따로 + // 일위대가 서고(50-3 · 134-3) — 맨 위 칸 · 층은 페이지 층과 따로 const precombo = buildPrecomboSection({ ...opt.precombo, confirmLeave: opt.confirmLeave, onOpen: opt.onOpen, getOpen: opt.getOpen, }); - const precomboSec = section(PRECOMBO_TITLE, precombo.body); - // 실무자료로 사전조합을 만들었음(확인 창 · 엑셀 보기 창) → 목록 다시 · 이 패널이 내려갔으면 듣기 끝 + const precomboSec = section(L("M02_PrecomboTitle"), precombo.body); + // 실무자료로 일위대가를 만들었음(확인 창 · 엑셀 보기 창) → 목록 다시 · 이 패널이 내려갔으면 듣기 끝 const onBuilt = (): void => precombo.body.isConnected ? void precombo.refresh() : document.removeEventListener(HOPYO_BUILT, onBuilt); document.addEventListener(HOPYO_BUILT, onBuilt); - // 구조물 옵션 정의 · 도면 양식은 시스템 층(마스터 템플릿)에만 · 정의 고쳐 씀은 시스템 관리자만 · 다른 역할은 읽기만(PLAN 48-2) - const extras = + // 도면 양식은 시스템 층(마스터 템플릿)에만 + const drawing = layer === "system" - ? { - 옵션: section(options.title, options.body), - 도면: section(L("M02_KindDrawing"), bodies.get("drawing")!, ...actionRow("drawing")), - } + ? section(L("M02_KindDrawing"), bodies.get("drawing")!, ...actionRow("drawing")) : null; // 실무문서(옛 마스터 일위대가 조합 · PLAN 39-2) — 구조물 조합 밖 · 도면 양식 아래 · 마스터 층 · 시스템 관리자만 const practice = @@ -171,23 +158,24 @@ export function buildSide(opt: SideOptions): SideHandle { ? buildPracticeSection({ confirmLeave: opt.confirmLeave, show: opt.showView, - onOpen: () => (options.clear(), paint()), + onOpen: paint, }) : null; - // 일위대가 실무자료(324-4) — 사전조합 아래 · 층 = 구조물 조합 층 · 쓰기 권한 없으면 올리기 꺼짐 + // 일위대가 불러오기(324-4 · 제목은 718-1) · 층 = 구조물 조합 층 · 쓰기 권한 없으면 올리기 꺼짐 const hopyo = buildHopyoSection({ canWrite: canEdit(layer, opt.isAdmin), confirmLeave: opt.confirmLeave, show: opt.showView, - onOpen: () => (options.clear(), practice?.clear(), paint()), + onOpen: () => (practice?.clear(), paint()), }); const listHost = el("div", { className: "m02-side__lists ui-sidebar-panel", children: [ - summary, precomboSec, + summary, + createDivider(), section(hopyo.title, hopyo.body), - ...(extras ? [extras.옵션, extras.도면].filter((s): s is HTMLElement => !!s) : []), + ...(drawing ? [drawing] : []), ...(practice ? [section(practice.title, practice.body)] : []), ], }); @@ -237,14 +225,13 @@ export function buildSide(opt: SideOptions): SideHandle { for (const [kind, b] of editButtons) b.copy.disabled = b.del.disabled = cur?.종류 !== kind; } - /** 고름 표시는 여기서만 — 목록 · 옵션 줄을 함께 그려야 두 줄이 동시에 안 남음 - * (양식이 열려 있으면 옵션 · 일위대가 조합 고름은 지움) */ + /** 고름 표시는 여기서만 — 목록 · 실무문서 줄을 함께 그려야 두 줄이 동시에 안 남음 + * (양식이 열려 있으면 실무문서 고름은 지움) */ function paint(): void { paintList(); precombo.paint(); hopyo.paint(); paintButtons(); - if (opt.getOpen()) options.clear(); if (opt.getOpen()) practice?.clear(); } @@ -347,10 +334,8 @@ export function buildSide(opt: SideOptions): SideHandle { } const reveal = (target: "옵션" | "도면" | "일위대가조합"): void => { - const sec = target === "일위대가조합" ? precomboSec : extras?.[target]; - if (!sec) return; // 옵션 보이기는 시스템 관리자만 - sec.classList.remove("is-collapsed"); - if (target === "옵션") options.body.querySelector("button")?.click(); + const sec = target === "일위대가조합" ? precomboSec : target === "도면" ? drawing : null; + sec?.classList.remove("is-collapsed"); }; void refresh(); diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Precombo.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Precombo.ts index e109cc347..38125c298 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Precombo.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Precombo.ts @@ -1,9 +1,9 @@ /* ============================================================================= * M02_MasterTemplete_UI_Side_Precombo.ts - * 좌측 「일위대가 사전조합」 컨테이너(50-3 ③ · 51-4 · 134-3) — 구조물 조합 아래 · 층 단추(페이지가 만듦 · - * 페이지 층과 따로 · 마스터 · 회사 · 내 개인) + 그 층 사전조합 나무. + * 좌측 「일위대가」 컨테이너(50-3 ③ · 51-4 · 134-3) — 구조물 조합 아래 · 층 단추(페이지가 만듦 · + * 페이지 층과 따로 · 마스터 · 회사 · 내 개인) + 그 층 일위대가 나무. * 나무 = 자유 폴더(158-1 · 구조물 조합 탐색기와 같은 꼴 · 깊이 자유 · 층마다 따로 · 자리 없는 문서 = 뿌리) — - * 폴더 오른쪽 클릭 [새 폴더] · [여기에 새 사전조합] · [이름 바꾸기] · [지우기](빈 폴더만) · 빈 자리 오른쪽 + * 폴더 오른쪽 클릭 [새 폴더] · [여기에 새 일위대가] · [이름 바꾸기] · [지우기](빈 폴더만) · 빈 자리 오른쪽 * 클릭 = 뿌리 메뉴 · 문서 · 폴더 끌어 옮기기. 폴더 자리만 서버(`precombo-tree`)에 적음 — 문서 파일은 안 옮김. * 구분 · 상세구분은 문서 칸(상세 머리 · 고르기 창 거름 · `precomboClasses`) — 나무 폴더와 무관. * 문서 누름 = 편집 칸(UI_Main 종류 precombo = 일위대가 상세 모드) · 켜진 층 단추 다시 누름 = 목록표 @@ -42,7 +42,6 @@ import { createEmpty } from "@ui/ui_template_empty"; export type PrecomboKind = Extract; -export const PRECOMBO_TITLE = "일위대가 사전조합"; export const isPrecombo = (kind: Kind): kind is PrecomboKind => kind === "precombo"; /** 여러 구조물에 쓰는 묶음(콘크리트 · 거푸집 등) — 군마다 첫 상세구분(134-5) */ @@ -183,7 +182,7 @@ export function buildPrecomboSection(opt: PrecomboOptions): PrecomboHandle { } }; - /** [여기에 새 사전조합] — 뿌리에 생성 → 폴더면 그 자리로 옮김 → 나무 다시 · 상세 엶 */ + /** [여기에 새 일위대가] — 뿌리에 생성 → 폴더면 그 자리로 옮김 → 나무 다시 · 상세 엶 */ const make = (path: string[]): void => { const at = opt.scope(); nameDialog(tc("Lib_NewHere"), "", async (name) => { @@ -303,7 +302,7 @@ export function buildPrecomboSection(opt: PrecomboOptions): PrecomboHandle { paint(); }; - // 빈 자리 오른쪽 클릭 = 뿌리 메뉴(새 폴더 · 새 사전조합) + // 빈 자리 오른쪽 클릭 = 뿌리 메뉴(새 폴더 · 새 일위대가) treeHost.addEventListener("contextmenu", (event) => { if (!view || (event.target as HTMLElement).closest("[data-node]")) return; event.preventDefault(); diff --git a/resources/tester/test_m02_precombo_scope.py b/resources/tester/test_m02_precombo_scope.py index fe9bd01fa..081da91c9 100644 --- a/resources/tester/test_m02_precombo_scope.py +++ b/resources/tester/test_m02_precombo_scope.py @@ -82,7 +82,7 @@ def test_사전조합_층_단추는_페이지를_다시_안_세움(): def test_고름에_owner_권한_서고_길에_명시_owner(): side = _text("M02_MasterTemplete_UI_Side_Precombo.ts") - assert 'export const PRECOMBO_TITLE = "일위대가 사전조합";' in side + assert "PRECOMBO_TITLE" not in side # 716-3 — 제목은 locale 키 「일위대가」 assert "owner: scope.owner," in side and "canWrite: scope.canWrite," in side assert "listTemplates(at.layer, null, at.owner)" in side main = _text("M02_MasterTemplete_UI_Main.ts") @@ -133,3 +133,20 @@ def test_목록표_빈_구분_미분류_고르기_창_새_분류_거름(): # 거름은 칸 값 그대로 — 품셈 구분을 고르면 서고 줄 빠지고 새 분류를 고르면 로직 줄 빠짐 assert "(!sub.select.value || x.row.구분 === sub.select.value)" in pick assert "(!sub.select.value || x.구분 === sub.select.value)" in pick + + +def test_왼쪽_칸_차례_일위대가_구조물조합_구분선_불러오기_716_3(): + side = _text("M02_MasterTemplete_UI_Side.ts") + order = side[side.index("m02-side__lists") :] + marks = [ + "precomboSec,", + "summary,", + "createDivider()", + "hopyo.title", + "drawing ?", + "practice ?", + ] + at = [order.index(m) for m in marks] + assert at == sorted(at) + + assert 'L("M02_PrecomboTitle")' in side and "buildOptionsSection" not in side diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index 0639ba79f..ce9385148 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -189,4 +189,5 @@ export const ui_locales_m2 = { "Practice documents changed — reloaded", ], B01_Dashboard_CardPrecombo: ["일위대가", "Unit-cost items"], + M02_PrecomboTitle: ["일위대가", "Unit-cost items"], } as const;