diff --git a/A00_Common/m_settings_nav.ts b/A00_Common/m_settings_nav.ts index fff8b3264..8c7cbeef3 100644 --- a/A00_Common/m_settings_nav.ts +++ b/A00_Common/m_settings_nav.ts @@ -65,6 +65,7 @@ export function settingLinks(user: SettingUser, companyId: number | string | nul m01("로직", L("B01_Dashboard_CardLogic"), read, "로직"), m01("옵션", L("B01_Dashboard_CardStructOptionDef"), desc, "옵션"), m01("별칭", L("B01_Dashboard_CardAliasLedger"), desc, "별칭"), + m02(L("B01_Dashboard_CardSangeun"), "단가산출", comboRoot("master"), desc), m02(L("B01_Dashboard_CardPrecombo"), "일위대가조합", comboRoot("master"), desc), m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", comboRoot("master"), desc), m02(L("B01_Dashboard_CardDrawing"), "도면", "master", desc), diff --git a/M02_MasterTemplete/M02_Hopyo_Line.py b/M02_MasterTemplete/M02_Hopyo_Line.py index 3cb722f25..501c1633a 100644 --- a/M02_MasterTemplete/M02_Hopyo_Line.py +++ b/M02_MasterTemplete/M02_Hopyo_Line.py @@ -251,7 +251,7 @@ def match_line(line: dict[str, Any], ctx: Ctx, full: bool = True) -> dict[str, A target = {"호표": no, "이름": h.get("이름", ""), "단위": h.get("단위"), "입력값": {}} target.update(못읽음=[], 출처={}, 환산=conv, 몫=None, 배=conv["배"]) ok = bool(made) and conv["배"] is not None and conv["갈래"] != "추정" - text = f"확인된 사전조합 {made}" if made else f"제{no}호표 먼저 확인" + text = f"확인된 일위대가 {made}" if made else f"제{no}호표 먼저 확인" return {**out, "상태": "호표", "체크": ok, "대상": [target], "글": text} if kind == "노임": return {**out, "상태": "노임", "글": "노임 · 그릇 없음"} diff --git a/M02_MasterTemplete/M02_Labor_Ref.py b/M02_MasterTemplete/M02_Labor_Ref.py index ae47ac4be..f8ede9600 100644 --- a/M02_MasterTemplete/M02_Labor_Ref.py +++ b/M02_MasterTemplete/M02_Labor_Ref.py @@ -299,6 +299,19 @@ def summary(folder: Path, kind: str, key: str, expand: Expand | None = None) -> layer, owner = where(folder) base = expand({"층": layer, "주인": owner, "키": key}).get("기준") or {} out["기준금액"] = float(base["계"]) if "계" in base else None + # 717-2a 목록 열 — 바탕(로직 「키 이름」 | 참조 키 | None) · 고친 줄 수 · 쓰는 곳 수 + if kind == "sangeun": + top, (layer, owner) = doc.get("바탕") or {}, where(folder) + logic = top.get("로직") + out["바탕"] = logic or (top.get("참조") or {}).get("키") + if logic and expand is not None: + try: + out["바탕"] = f"{logic} {expand.whole.logic(logic).get('이름') or ''}".strip() + except Exception: # noqa: BLE001 — 없는 로직 = 키만(셈 때 까닭) + pass + out["고친줄수"] = sum(len(doc.get(k) or ()) for k in ("덮개", "더한줄", "뺀줄")) + # ponytail: SK 마다 모든 층 훑음(users) — 목록이 느려지면 층 훑기 한 번 셈으로 + out["쓰는곳수"] = len(users({"층": layer, "주인": owner, "키": key})) return out diff --git a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts index 205097490..9d608315a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts @@ -10,8 +10,9 @@ export type Layer = "system" | "company" | "personal" | "project"; /** 서버 종류 — 화면의 「표 양식」 = table · 「도면 양식」 = drawing · 「구조물 도면」 = structure · * 「면적산출」 = area(구조물집계표 열마다 한 통합문서) · * 「일위대가」 = labor(열마다 문서 · 39장) · - * 「사전조합」 = precombo(일위대가 사전조합 서고 · 50-3 · 키가 이름 · 옛 단가승격 promote 는 134-2 에서 뺌) */ -export type Kind = "table" | "drawing" | "structure" | "area" | "labor" | "precombo"; + * 「사전조합」 = precombo(일위대가 사전조합 서고 · 50-3 · 키가 이름 · 옛 단가승격 promote 는 134-2 에서 뺌) · + * 「단가산출」 = sangeun(SK- 서고 · 715-2 · 717-2a) */ +export type Kind = "table" | "drawing" | "structure" | "area" | "labor" | "precombo" | "sangeun"; export interface TemplateInfo { 종류: Kind; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts index f293e9ce3..09f4e5166 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts @@ -26,6 +26,7 @@ import { } from "@ui/ui_template_elements"; import { loadingError, showLoading } from "@ui/ui_template_loading"; import { openModal } from "@ui/ui_template_modal"; +import { t as L } from "@ui/ui_template_locale"; import { createSourceView } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Source"; import { buildHopyo, @@ -429,7 +430,7 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement { const ask = whole ? `호표 통째로 만듦 — ${h.통째!.대상.map(targetText).join(" · ")}` : `품셈 줄 ${put.length}${diff ? `(품 다름 ${diff})` : ""} · 고정 줄 ${h.줄.length - put.length - outs}` + - ` · 뺌 ${outs} — ${money}(미리 셈 · 끝수 차 있음) — 사전조합 생성`; + ` · 뺌 ${outs} — ${money}(미리 셈 · 끝수 차 있음) — ${L("M02_PrecomboTitle")} 생성`; if (!(await showConfirmDialog(ask, "생성"))) return; const from = { 층: opt.scope.layer, 주인: opt.scope.owner, 키: opt.key, 번: opt.no }; showLoadingOverlay(); @@ -530,7 +531,11 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement { draw(); }); const whole = !!h.통째 && (d.통째 ?? h.통째.체크); - const go = createButton({ label: "사전조합 생성", variant: "filled", size: "sm" }); + const go = createButton({ + label: `${L("M02_PrecomboTitle")} 생성`, + variant: "filled", + size: "sm", + }); go.setAttribute("data-hopyo-make", ""); go.addEventListener("click", () => void make(h)); tools.push(all); @@ -573,7 +578,7 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement { ...(h.만든 ? [ note( - `만든 사전조합 ${h.만든.키}${h.만든.확인 ? " · 확인함" : ""}` + + `만든 ${L("M02_PrecomboTitle")} ${h.만든.키}${h.만든.확인 ? " · 확인함" : ""}` + (h.만든.품셈 ? ` · 품셈 줄 ${h.만든.품셈.length} · 고정 줄 ${h.만든.고정 ?? 0}` : ""), diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_CheckRows.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_CheckRows.ts index 5b3d90d3a..577378746 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_CheckRows.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_CheckRows.ts @@ -12,6 +12,7 @@ * ========================================================================== */ import { createButton, el } from "@ui/ui_template_elements"; +import { t as L } from "@ui/ui_template_locale"; import type { HopyoLine, HopyoPart } from "./M02_MasterTemplete_Hopyo_Api"; import { checkable, @@ -170,7 +171,10 @@ function actsCell(line: HopyoLine, parts: HopyoPart[], ctx: RowsContext): HTMLEl createButton({ label, variant: "ghost", size: "sm", onClick: run }); const out = el("select", { className: "ui-input ui-input--sm", - attrs: { title: "로직없음 · 사용안함 — 사전조합에 안 넣고 장부에 남김", "data-out": "" }, + attrs: { + title: `로직없음 · 사용안함 — ${L("M02_PrecomboTitle")}에 안 넣고 장부에 남김`, + "data-out": "", + }, children: OUT_TEXT.map(([value, text]) => el("option", { text, attrs: { value } })), }) as HTMLSelectElement; out.value = ctx.d.뺌[line.차례] ?? ""; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts index 0407a7891..f4aaa3a11 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts @@ -11,6 +11,7 @@ import { createButton, el, showToast } from "@ui/ui_template_elements"; import { loadingError, showLoading } from "@ui/ui_template_loading"; +import { t as L } from "@ui/ui_template_locale"; import { readLedger, saveLedger, @@ -326,7 +327,7 @@ export function buildHopyoLedger(opt: LedgerOptions): HTMLElement { className: "ui-toolbar m02-hopyo__bar", children: [ el("strong", { text: LEDGER_TITLE }), - note(`${LEDGER_HINT} · 사전조합 생성 때 남음 · 아래 층이 이김`), + note(`${LEDGER_HINT} · ${L("M02_PrecomboTitle")} 생성 때 남음 · 아래 층이 이김`), search, kind, ...(opt.canWrite ? [save, drop] : []), diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Pick.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Pick.ts index def3c122d..a24be99b8 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Pick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Pick.ts @@ -7,6 +7,7 @@ * ========================================================================== */ import { showToast } from "@ui/ui_template_elements"; +import { t as L } from "@ui/ui_template_locale"; import { listHopyoPicks, type HopyoPickRow } from "./M02_MasterTemplete_Hopyo_Api"; import type { LibraryPick, PickExtra } from "./M02_MasterTemplete_Labor_Pick"; import { tc } from "./M02_MasterTemplete_Labor_Text"; @@ -82,7 +83,7 @@ export function hopyoExtra(row: HopyoPickRow, lib: LibraryPick): PickExtra { export async function pickHopyo(row: HopyoPickRow, lib: LibraryPick): Promise { const alias = row.이름.slice(0, ALIAS_MAX).trim() || null; if (row.확인) { - showToast(`「${row.이름}」 → 확인한 사전조합 ${row.확인.키}`, "success"); + showToast(`「${row.이름}」 → 확인한 ${L("M02_PrecomboTitle")} ${row.확인.키}`, "success"); return lib.pick(row.확인, row.이름, alias); } try { diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Relink.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Relink.ts index 128d7003a..8f038fb87 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Relink.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Relink.ts @@ -12,6 +12,7 @@ import { createButton, showToast } from "@ui/ui_template_elements"; import { openModal } from "@ui/ui_template_modal"; import { createDataTable, type DataRow } from "@ui/ui_template_data_table"; import { createText } from "@ui/ui_template_text"; +import { t as L } from "@ui/ui_template_locale"; import { pageScope, relinkApply, @@ -43,7 +44,7 @@ const won = (v: number | null | undefined): string => export function relinkKeys(keys: string[]): RelinkSource { const scope = pageScope(); return { - title: `이 공사 단가로 다시 잇기 — 사전조합 ${keys.length}`, + title: `이 공사 단가로 다시 잇기 — ${L("M02_PrecomboTitle")} ${keys.length}`, priceLabel: "최신 판 단가", rows: () => relinkRows(scope, { 키: keys }), apply: async (changes) => { @@ -51,7 +52,7 @@ export function relinkKeys(keys: string[]): RelinkSource { const sum = (k: "전" | "후"): number => got.문서.reduce((a, d) => a + (d[k] ?? 0), 0); return { back: got.되돌림, - note: `바뀐 줄 ${got.바뀜} · 사전조합 ${got.문서.length} · 1 단위 값 합 ${won(sum("전"))} → ${won(sum("후"))}`, + note: `바뀐 줄 ${got.바뀜} · ${L("M02_PrecomboTitle")} ${got.문서.length} · 1 단위 값 합 ${won(sum("전"))} → ${won(sum("후"))}`, }; }, }; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts index ddb3f3de5..8fadc13bf 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts @@ -24,6 +24,7 @@ import { showToast, } from "@ui/ui_template_elements"; import { openModal } from "@ui/ui_template_modal"; +import { t as L } from "@ui/ui_template_locale"; import type { Sheet } from "../A00_Common/spreadsheet/spreadsheet_types"; import type { SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet"; import { StaleError } from "./M02_MasterTemplete_Api_Fetch"; @@ -239,7 +240,7 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void { variant: "danger", size: "sm", onClick: async () => { - const ask = `실무자료 「${opt.파일}」을 지움 — 만든 사전조합 · 별칭 장부는 그대로`; + const ask = `실무자료 「${opt.파일}」을 지움 — 만든 ${L("M02_PrecomboTitle")} · 별칭 장부는 그대로`; if (!(await showConfirmDialog(ask, "삭제"))) return; try { await deleteHopyo(opt.scope, opt.key); @@ -427,7 +428,7 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void { const nos = [...checked].sort((a, b) => a - b); const fresh = nos.filter((no) => !hasDraft(draftKey(opt.scope, opt.key, no))).length; const ask = - `고른 호표 ${nos.length}개를 체크한 줄로 사전조합 생성` + + `고른 호표 ${nos.length}개를 체크한 줄로 ${L("M02_PrecomboTitle")} 생성` + (fresh ? ` — 확인 창에서 안 고친 ${fresh}개는 서버 제안 체크 줄` : ""); if (!(await showConfirmDialog(ask, "저장"))) return; go.disabled = true; @@ -510,7 +511,10 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void { } if (ok) document.dispatchEvent(new Event(HOPYO_BUILT)); result.replaceChildren( - table(["호표", "사전조합", "빠진 줄", "값 차이(참고)"], [...rows, ...skipped]), + table( + ["호표", `${L("M02_PrecomboTitle")}`, "빠진 줄", "값 차이(참고)"], + [...rows, ...skipped], + ), ); const all = order.length + skipped.length; showToast(`만듦 ${ok} / ${all}`, ok === all ? "success" : "warning"); @@ -526,7 +530,7 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void { /** [모두 고정으로 들여오기](705-3) — 물음 한 번 · 서버가 호표 모두를 하위부터(숫자 = 서버가 파일에서) */ async function importAll(): Promise { const ask = - `호표 ${briefs.length}개를 엑셀 숫자 그대로(고정 줄) 사전조합으로 들여옴 — ` + + `호표 ${briefs.length}개를 엑셀 숫자 그대로(고정 줄) ${L("M02_PrecomboTitle")}으로 들여옴 — ` + "이미 만든 호표는 그대로 · 품셈 맞춤은 뒤에 호표 [수정]에서 [품셈으로 바꾸기]"; if (!(await showConfirmDialog(ask, "들여오기"))) return; fixAll.disabled = true; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_ToItem.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_ToItem.ts index 01f117d13..708ce9d96 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_ToItem.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_ToItem.ts @@ -17,6 +17,7 @@ import { createCheckbox } from "@ui/ui_template_checkbox"; import { createInputField } from "@ui/ui_template_input"; import { openModal } from "@ui/ui_template_modal"; import { createText } from "@ui/ui_template_text"; +import { t as L } from "@ui/ui_template_locale"; import { buildHopyoItem, hopyoSiblings, @@ -101,7 +102,7 @@ export async function toItem(o: ToItemOptions): Promise { actions.append(go); body.append( createText( - "체크한 줄 = 품셈 줄 · 나머지 = 고정 줄(엑셀 숫자 그대로) — 사전조합을 거치지 않고 구조물 조합 항목을 바로 만듦", + `체크한 줄 = 품셈 줄 · 나머지 = 고정 줄(엑셀 숫자 그대로) — ${L("M02_PrecomboTitle")}을 거치지 않고 구조물 조합 항목을 바로 만듦`, "muted", ), name.root, diff --git a/M02_MasterTemplete/M02_MasterTemplete_Layer.ts b/M02_MasterTemplete/M02_MasterTemplete_Layer.ts index 242e0fa69..721f67c2a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Layer.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Layer.ts @@ -1,7 +1,7 @@ /* ============================================================================= * M02_MasterTemplete_Layer.ts * M02 한 화면의 뿌리 셋(마스터 · 회사 · 개인 · PLAN 27-1) — 진입 해시 - * `#/m02-master-template?open=구조물조합|옵션|도면&root=master|company|personal&owner=…&company=…&pre=…` + * `#/m02-master-template?open=구조물조합|옵션|도면|일위대가조합|단가산출&root=master|company|personal&owner=…&company=…&pre=…` * (옛 `?layer=` 도 root 로 읽음 · `pre` = 일위대가 사전조합 층 · 134-3) · 고칠 권한(`accessFor` — 서버 `M02_Layer_Access` 와 짝 · 27-1c) · * 회사 없음 안내 · 회사용 읽기만일 때 [개인용 복사] 막대. * ========================================================================== */ @@ -16,7 +16,7 @@ import { copyItemToPersonal, fetchStructureCombos } from "./M02_MasterTemplete_A export type ManageLayer = "system" | "company" | "personal"; /** 해시 · 화면의 뿌리 이름 — 서버 층 이름은 `layerOfRoot` */ export type Root = "master" | "company" | "personal"; -export type OpenTarget = "구조물조합" | "옵션" | "도면" | "일위대가조합"; +export type OpenTarget = "구조물조합" | "옵션" | "도면" | "일위대가조합" | "단가산출"; export interface Entry { root: Root; @@ -29,7 +29,7 @@ export interface Entry { pre: Root | null; } -const OPENS: OpenTarget[] = ["구조물조합", "옵션", "도면", "일위대가조합"]; +const OPENS: OpenTarget[] = ["구조물조합", "옵션", "도면", "일위대가조합", "단가산출"]; const ROOTS: Root[] = ["master", "company", "personal"]; export const layerOfRoot = (root: Root): ManageLayer => (root === "master" ? "system" : root); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Library_List.ts b/M02_MasterTemplete/M02_MasterTemplete_Library_List.ts index 7a1be7e48..7a3446ca4 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Library_List.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Library_List.ts @@ -1,7 +1,8 @@ /* ============================================================================= * M02_MasterTemplete_Library_List.ts - * 일위대가 사전조합 서고 목록표(51-4) — 좌측 사전조합 층 단추를 누르면 상세 칸에 뜸. - * 표 = 고름 · 키 · 구분 · 상세구분(51-12) · 이름 · 단위 · 기준 금액(서버 1 단위 계) · 고친 날 · 줄 누름 = 그 문서 상세. + * 서고 목록표(51-4 · 717-2a) — 좌측 일위대가 · 단가산출 층 단추를 누르면 상세 칸에 뜸. + * 표 = 고름 · 키 · 구분 · 상세구분(51-12) · 이름 · 단위 · 기준 금액(서버 1 단위 계) · 고친 날 · 줄 누름 = 그 문서 상세 · + * 단가산출 = 구분 둘 대신 바탕(M01 로직 · 없으면 「바탕 없음」) · 고친 줄 수 · 쓰는 곳 수(서버 `summary`). * 머리 [생성] · [복사](고른 줄) · [삭제](고른 줄 · 쓰는 곳 있으면 서버 409 까닭 그대로). * 꼴 = M01 단가산출 로직 목록 — 찾기 줄(`buildHeadFilters`) · 쪽(`buildPager`) · 표(`m01-master__grid`) 를 M01 에서 import. * ========================================================================== */ @@ -28,10 +29,10 @@ import { saveTemplate, type Layer, type LibraryBody, + type LibraryKind, type TemplateInfo, } from "./M02_MasterTemplete_Api_Fetch"; import { tc } from "./M02_MasterTemplete_Labor_Text"; -import type { PrecomboKind } from "./M02_MasterTemplete_UI_Side_Precombo"; /** 목록 줄 — 서버가 문서 `이름`(제목) · 단위 · 1 단위 기준 계를 실어 보냄 */ type Row = TemplateInfo & { @@ -40,13 +41,16 @@ type Row = TemplateInfo & { 기준금액?: number | null; 구분?: string | null; 상세구분?: string | null; + 바탕?: string | null; + 고친줄수?: number; + 쓰는곳수?: number; }; export interface LibraryListOptions { layer: Layer; /** 그 층 주인(134-3 · 사전조합 층 · null = 자기 층) */ owner: string | null; - kind: PrecomboKind; + kind: LibraryKind; canEdit: boolean; /** 줄 누름 · 생성 · 복사 뒤 — 그 문서 상세 */ open: (key: string, label: string) => void; @@ -60,6 +64,36 @@ const money = (v: number | null | undefined): string => v == null ? "" : v.toLocaleString("ko-KR", { maximumFractionDigits: 2 }); export const libraryLabel = (row: Row): string => (row.제목 ? `${row.이름} ${row.제목}` : row.이름); +/** 새 단가산출 몸(717-2a) — 바탕 없음 · 빈 더한줄 · 단위 「식」(서버 필수 · 상세에서 고침 · 717-2b) */ +export const sangeunBlank = (name: string): Record => ({ + 이름: name, + 단위: "식", + 바탕: null, + 더한줄: [], +}); +/** 표 열 — 머리 글 · 줄 값(일위대가 = 구분 둘 · 단가산출 = 바탕 · 고친 줄 · 쓰는 곳) */ +const COLUMNS: Record [string, (row: Row) => string][]> = { + precombo: () => [ + [tc("Lib_Key"), (r) => r.이름], + [tc("Sub"), (r) => r.구분 || tc("Lib_Unsorted")], // 빈 구분 = 미분류(149-2 · 나무와 같은 말) + [tc("Detail"), (r) => r.상세구분 ?? ""], + [tc("Lib_Name"), (r) => r.제목 ?? ""], + [tc("Lib_Unit"), (r) => r.단위 ?? ""], + [tc("Lib_Base"), (r) => money(r.기준금액)], + [tc("Lib_Date"), (r) => r.수정일], + ], + sangeun: () => [ + [tc("Lib_Key"), (r) => r.이름], + [tc("Lib_Name"), (r) => r.제목 ?? ""], + [tc("Lib_Unit"), (r) => r.단위 ?? ""], + [L("M02_SangeunBase"), (r) => r.바탕 || L("M02_SangeunNoBase")], + [L("M02_SangeunEdited"), (r) => String(r.고친줄수 ?? 0)], + [L("M02_SangeunUsers"), (r) => String(r.쓰는곳수 ?? 0)], + [tc("Lib_Base"), (r) => money(r.기준금액)], + [tc("Lib_Date"), (r) => r.수정일], + ], +}; + /** 이름 창(생성 · 복사 · 사전조합 나무 폴더 메뉴 149-1) — [생성] = `onOk` 성공하면 닫힘 */ export const nameDialog = ( title: string, @@ -101,6 +135,7 @@ export function mountLibraryList( tools: HTMLElement, opt: LibraryListOptions, ): { getDoc: () => null; destroy: () => void } { + const columns = COLUMNS[opt.kind](); let rows: Row[] = []; let picked: string | null = null; let page = 1; @@ -144,15 +179,9 @@ export function mountLibraryList( attrs: { "data-lib-key": row.이름 }, children: [ pick, - ...[ - row.이름, - row.구분 || tc("Lib_Unsorted"), // 빈 구분 = 미분류(149-2 · 나무와 같은 말) - row.상세구분 ?? "", - row.제목 ?? "", - row.단위 ?? "", - money(row.기준금액), - row.수정일, - ].map((v) => el("td", { text: v, attrs: { title: v } })), + ...columns + .map(([, value]) => value(row)) + .map((v) => el("td", { text: v, attrs: { title: v } })), ], }); tr.addEventListener("click", () => opt.open(row.이름, libraryLabel(row))); @@ -160,7 +189,12 @@ export function mountLibraryList( }) : [ el("tr", { - children: [el("td", { text: L("M02_NoTemplates"), attrs: { colspan: "8" } })], + children: [ + el("td", { + text: L("M02_NoTemplates"), + attrs: { colspan: String(columns.length + 1) }, + }), + ], }), ]), ); @@ -191,7 +225,10 @@ export function mountLibraryList( /** 사전조합 = 이름 · 구분 · 상세구분 빈 값(미분류 · 134-5 · 상세에서 고름) · 빈 묶음 */ btnNew.addEventListener("click", () => { nameDialog(L("M02_NewTitle"), "", async (name) => { - const 문서 = { 이름: name, 구분: "", 상세구분: "", 단위: null, 담은로직: [] }; + const 문서 = + opt.kind === "sangeun" + ? sangeunBlank(name) + : { 이름: name, 구분: "", 상세구분: "", 단위: null, 담은로직: [] }; await create({ 문서 }, name); }); }); @@ -244,16 +281,7 @@ export function mountLibraryList( page = 1; draw(); }); - const heads = [ - "", - "Lib_Key", - "Sub", - "Detail", - "Lib_Name", - "Lib_Unit", - "Lib_Base", - "Lib_Date", - ] as const; + const heads = ["", ...columns.map(([head]) => head)]; const gridBox = el("div", { className: "ui-table-wrap m01-master__grid-wrap", attrs: { "data-lib-list": opt.kind }, @@ -262,9 +290,7 @@ export function mountLibraryList( className: "ui-table m01-master__grid", children: [ el("thead", { - children: [ - el("tr", { children: heads.map((h) => el("th", { text: h ? tc(h) : "" })) }), - ], + children: [el("tr", { children: heads.map((h) => el("th", { text: h })) })], }), body, ], diff --git a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_View.ts b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_View.ts new file mode 100644 index 000000000..da3307701 --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_View.ts @@ -0,0 +1,61 @@ +/* ============================================================================= + * M02_MasterTemplete_Sangeun_View.ts + * 단가산출(SK) 상세 자리(717-2a) — 편집 칸 종류 sangeun 이 여는 곳(UI_Main 갈래 한 줄). + * 지금 = 이름 · 단위 · 바탕 · 고친 줄 요약(덮개 · 더한 줄 · 뺀 줄 · 열린 입력) + 상세 자리표. + * 717-2b(sub2) = 자리표 줄을 `mountSangeunDetail(slot, doc, opt)`(`Sangeun_Detail.ts`)로 갈음 · + * [저장]은 그 손잡이 `getDoc` · 고침 알림은 `opt.onChanged`. 지금 [저장] = 읽은 문서 그대로. + * ========================================================================== */ + +import { el } from "@ui/ui_template_elements"; +import { createEmpty } from "@ui/ui_template_empty"; +import { createKv } from "@ui/ui_template_kv"; +import { t as L } from "@ui/ui_template_locale"; +import { tc } from "./M02_MasterTemplete_Labor_Text"; +import type { Selection } from "./M02_MasterTemplete_UI_Main"; + +/** SK 문서 중 이 자리가 읽는 칸(꼴 = 서버 `M02_Sangeun_Doc`) */ +export interface SangeunDoc { + 키?: string; + 이름?: string; + 단위?: string; + 바탕?: { 로직?: string; 참조?: { 키?: string } } | null; + 덮개?: Record; + 더한줄?: unknown[]; + 뺀줄?: unknown[]; + 열린입력?: string[]; +} + +/** 고름(층 · 주인 · SK 키 = name) + 읽기만 · 고침 알림 */ +export type SangeunViewOptions = Pick & { + readOnly: boolean; + onChanged: (dirty: boolean) => void; +}; + +/** 바탕 한 줄 — 로직 키 · 참조 SK 키 · 없으면 「바탕 없음」 */ +export const baseText = (doc: SangeunDoc): string => + doc.바탕?.로직 ?? doc.바탕?.참조?.키 ?? L("M02_SangeunNoBase"); + +export function mountSangeunView( + host: HTMLElement, + doc: unknown, + _opt: SangeunViewOptions, +): { getDoc: () => unknown; destroy: () => void } { + const sk = (doc ?? {}) as SangeunDoc; + const count = (v: unknown[] | Record | undefined): number => + Array.isArray(v) ? v.length : Object.keys(v ?? {}).length; + const head = createKv([ + { label: L("M02_Name"), value: sk.이름 ?? "" }, + { label: tc("Lib_Unit"), value: sk.단위 ?? "" }, + { label: L("M02_SangeunBase"), value: baseText(sk) }, + { label: L("M02_SangeunCover"), value: count(sk.덮개) }, + { label: L("M02_SangeunAdded"), value: count(sk.더한줄) }, + { label: L("M02_SangeunRemoved"), value: count(sk.뺀줄) }, + { label: L("M02_SangeunOpened"), value: (sk.열린입력 ?? []).join(" · ") || null }, + ]); + const slot = el("div", { + attrs: { "data-sangeun-detail": sk.키 ?? "" }, + children: [createEmpty({ text: L("M02_SangeunDetailSoon") })], + }); + host.replaceChildren(el("div", { attrs: { "data-sangeun-view": "" }, children: [head, slot] })); + return { getDoc: () => doc, destroy: () => host.replaceChildren() }; +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index a0a616c8f..8a9182d22 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -357,6 +357,12 @@ export function createMain( const readOnly = !writable(at); const mine = seq; // 모듈 · 집계표 번호를 기다리는 사이 다른 화면(옵션 정의 등)이 열렸으면 그만둠 host.replaceChildren(); + if (at.kind === "sangeun") { + const { mountSangeunView } = await import("./M02_MasterTemplete_Sangeun_View"); // 717-2a + const onChanged = (d: boolean): boolean => (dirty = d && !readOnly); + if (mine === seq) editor = mountSangeunView(host, doc, { ...at, readOnly, onChanged }); + return; + } if (at.kind === "table") { const create = await findExport<(h: HTMLElement, d: unknown, o: object) => EditorHandle>( SHEET_MODS, @@ -503,7 +509,7 @@ export function createMain( return empty(""); } title.textContent = isCombo(next) ? L("M02_KindTable") : (next.label ?? next.name); // 26-7a - if (isPrecombo(next.kind) && !next.name) { + if ((isPrecombo(next.kind) || next.kind === "sangeun") && !next.name) { // 서고 목록표(51-4) — 이름 "" · 저장할 것 없음 · 줄 누름 = 그 문서 상세 save.hidden = true; const { mountLibraryList } = await import("./M02_MasterTemplete_Library_List"); diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts index 436262468..4ff1e2265 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts @@ -53,6 +53,7 @@ import { canEdit, createMain, newItemButton, type Selection } from "./M02_Master import { buildSide } from "./M02_MasterTemplete_UI_Side"; import { ITEM_BUILT } from "./M02_MasterTemplete_Hopyo_ToItem"; import { precomboList, type PrecomboScope } from "./M02_MasterTemplete_UI_Side_Precombo"; +import { sangeunList } from "./M02_MasterTemplete_UI_Side_Sangeun"; import { buildTwoWay, pickedName } from "./M02_MasterTemplete_ItemPick"; import "./M02_MasterTemplete_UI_Style.css"; @@ -248,6 +249,16 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise void main.confirmLeave().then((ok) => ok && void main.open(precomboList(pre))), }); + // 단가산출 층(717-2a) — 일위대가 층과 따로 · 기본 개인(못 가면 마스터) · 주소에 안 실음 + let sk = preScope("personal"); + const skBar = (): HTMLElement => + createLayerBar({ + user, + active: { root: sk.root, owner: sk.owner }, + company, + go: (next) => ((sk = preScope(next)), void side.refreshSangeun()), + reselect: () => void main.confirmLeave().then((ok) => ok && void main.open(sangeunList(sk))), + }); const side = buildSide({ layer, isAdmin, @@ -264,6 +275,7 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise share.precomboMenu(pre.root === "personal" && !pre.owner, key, name), }, + sangeun: { scope: () => sk, bar: skBar }, }); const layout = el("div", { className: "ui-workflow-layout m02-master" }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts index 7f1d5b88d..5a86c8d5a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts @@ -1,8 +1,8 @@ /* ============================================================================= * M02_MasterTemplete_UI_Side.ts - * 마스터 템플릿 좌측 패널 차례(716-3) — 「일위대가」(층 단추 따로 · 2단 폴더 나무 · `UI_Side_Precombo` · + * 마스터 템플릿 좌측 패널 차례(716-3 · 717-2a) — 「단가산출」(층 단추 따로 · 기본 개인 · `UI_Side_Sangeun`) › 「일위대가」(층 단추 따로 · 2단 폴더 나무 · `UI_Side_Precombo` · * 50-3 · 134-3) › 「구조물 조합」(층 단추 · 나무) › 수평 구분선 › 「일위대가 불러오기」(구조물 조합 층 · - * `Hopyo_Side` · 324-4) › 「도면 양식」(새로 · 본떠 · 지우기) › 실무문서. 「단가산출」 칸은 맨 위(717-2a). + * `Hopyo_Side` · 324-4) › 「도면 양식」(새로 · 본떠 · 지우기) › 실무문서. * 구조물 옵션 정의는 M01 로 옮김(716-1). * 서버 길은 계약 `6_계약.md`. * 구조물 도면 · 일위대가 · 면적산출 상세는 좌측 줄이 아니라 항목 카드 [수정]으로(PLAN 45-2). @@ -36,6 +36,7 @@ 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, type PrecomboOptions } from "./M02_MasterTemplete_UI_Side_Precombo"; +import { buildSangeunSection, type SangeunOptions } from "./M02_MasterTemplete_UI_Side_Sangeun"; import { createEmpty } from "@ui/ui_template_empty"; import { createDivider } from "@ui/ui_template_divider"; @@ -67,6 +68,8 @@ export interface SideOptions { tree?: HTMLElement[]; /** 「일위대가」 층(134-3 · 페이지 층과 따로 · 층 단추는 페이지가 만듦) */ precombo: Pick; + /** 「단가산출」 층(717-2a · 일위대가 층과 따로 · 층 단추는 페이지가 만듦) */ + sangeun: Pick; } export interface SideHandle { @@ -75,9 +78,11 @@ export interface SideHandle { /** 고름 표시를 지금 열린 것 한 줄로 — 편집 칸이 무엇을 열든(카드 [도면 수정] 등) 부름 */ paint: () => void; /** 진입 해시 `open`(PLAN 27-1d) — 그 컨테이너를 펼침 · 도면은 시스템 층에만 · 옵션은 M01 로 옮겨 아무 일 없음 */ - reveal: (target: "옵션" | "도면" | "일위대가조합") => void; + reveal: (target: "옵션" | "도면" | "일위대가조합" | "단가산출") => void; /** 사전조합 층이 바뀜(134-3) — 그 컨테이너만 다시 그림 */ refreshPrecombo: () => Promise; + /** 단가산출 층이 바뀜(717-2a) — 그 컨테이너만 다시 그림 */ + refreshSangeun: () => Promise; } /** 서버 이름 규칙과 같게 — 화면에서 먼저 막음 */ @@ -133,7 +138,7 @@ 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, @@ -141,10 +146,18 @@ export function buildSide(opt: SideOptions): SideHandle { getOpen: opt.getOpen, }); const precomboSec = section(L("M02_PrecomboTitle"), precombo.body); + // 단가산출 서고(717-2a) — 맨 위 칸 · 층은 일위대가 층과 따로 + const sangeun = buildSangeunSection({ + ...opt.sangeun, + confirmLeave: opt.confirmLeave, + onOpen: opt.onOpen, + getOpen: opt.getOpen, + }); + const sangeunSec = section(L("M02_SangeunTitle"), sangeun.body); // 실무자료로 일위대가를 만들었음(확인 창 · 엑셀 보기 창) → 목록 다시 · 이 패널이 내려갔으면 듣기 끝 const onBuilt = (): void => precombo.body.isConnected - ? void precombo.refresh() + ? void Promise.all([precombo.refresh(), sangeun.refresh()]) : document.removeEventListener(HOPYO_BUILT, onBuilt); document.addEventListener(HOPYO_BUILT, onBuilt); // 도면 양식은 시스템 층(마스터 템플릿)에만 @@ -171,6 +184,7 @@ export function buildSide(opt: SideOptions): SideHandle { const listHost = el("div", { className: "m02-side__lists ui-sidebar-panel", children: [ + sangeunSec, precomboSec, summary, createDivider(), @@ -229,6 +243,7 @@ export function buildSide(opt: SideOptions): SideHandle { * (양식이 열려 있으면 실무문서 고름은 지움) */ function paint(): void { paintList(); + sangeun.paint(); precombo.paint(); hopyo.paint(); paintButtons(); @@ -253,7 +268,7 @@ export function buildSide(opt: SideOptions): SideHandle { if (mine !== seq) return; paintList(); paintButtons(); - await precombo.refresh(); + await Promise.all([precombo.refresh(), sangeun.refresh()]); } /* --- 시스템 층 새로 · 본떠 · 지우기(도면 양식만 — 구조물 도면은 집계표 열이 정함) --- */ @@ -333,11 +348,25 @@ export function buildSide(opt: SideOptions): SideHandle { } } - const reveal = (target: "옵션" | "도면" | "일위대가조합"): void => { - const sec = target === "일위대가조합" ? precomboSec : target === "도면" ? drawing : null; + const reveal = (target: "옵션" | "도면" | "일위대가조합" | "단가산출"): void => { + const sec = + target === "단가산출" + ? sangeunSec + : target === "일위대가조합" + ? precomboSec + : target === "도면" + ? drawing + : null; sec?.classList.remove("is-collapsed"); }; void refresh(); - return { root, refresh, paint, reveal, refreshPrecombo: precombo.refresh }; + return { + root, + refresh, + paint, + reveal, + refreshPrecombo: precombo.refresh, + refreshSangeun: sangeun.refresh, + }; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Sangeun.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Sangeun.ts new file mode 100644 index 000000000..724755454 --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Sangeun.ts @@ -0,0 +1,41 @@ +/* ============================================================================= + * M02_MasterTemplete_UI_Side_Sangeun.ts + * 좌측 맨 위 「단가산출」 칸(717-2a · 요구서 4장 12) — 서고 칸(`UI_Side_Library` · 716-4)에 단가산출 몫만 줌: + * 종류 sangeun(SK-) · 그 층 목록표 고름 · 새 문서 몸 · 글. 층 단추는 페이지가 만듦(칸마다 따로 · 기본 개인). + * 문서 누름 = 편집 칸(UI_Main 종류 sangeun = `Sangeun_View`) · 켜진 층 단추 다시 누름 = 목록표(`Library_List`). + * ========================================================================== */ + +import { t as L } from "@ui/ui_template_locale"; +import { sangeunBlank } from "./M02_MasterTemplete_Library_List"; +import type { Selection } from "./M02_MasterTemplete_UI_Main"; +import { + buildLibrarySection, + type LibraryHandle, + type LibraryOptions, + type LibraryScope, +} from "./M02_MasterTemplete_UI_Side_Library"; + +/** 그 층 목록표 고름(켜진 층 단추 다시 누름) */ +export const sangeunList = (scope: LibraryScope): Selection => ({ + layer: scope.layer, + owner: scope.owner, + canWrite: scope.canWrite, + projectId: null, + kind: "sangeun", + name: "", + label: L("M02_SangeunTitle"), +}); + +export type SangeunOptions = Pick< + LibraryOptions, + "scope" | "bar" | "confirmLeave" | "onOpen" | "getOpen" +>; + +export const buildSangeunSection = (opt: SangeunOptions): LibraryHandle => + buildLibrarySection({ + ...opt, + kind: "sangeun", + list: sangeunList, + blank: sangeunBlank, + text: { none: L("M02_SangeunNone"), newHere: L("M02_SangeunNewHere") }, + }); diff --git a/resources/tester/test_197_1_dashboard_option_card.py b/resources/tester/test_197_1_dashboard_option_card.py index fe1701798..2fe8b5116 100644 --- a/resources/tester/test_197_1_dashboard_option_card.py +++ b/resources/tester/test_197_1_dashboard_option_card.py @@ -49,16 +49,17 @@ def test_줄_해시_옵션은_M01(): from test_b01_settings_cards import _cards cards = _cards({"role": "SYSTEM_ADMIN", "is_master": False}, 3) - heads = [c[2].split("?")[0] + "?" + c[2].split("?")[1].split("&")[0] for c in cards[1:7]] + heads = [c[2].split("?")[0] + "?" + c[2].split("?")[1].split("&")[0] for c in cards[1:8]] assert heads == [ "m01-master-data?open=로직", "m01-master-data?open=옵션", "m01-master-data?open=별칭", # 718-3 + "m02-master-template?open=단가산출", # 717-2a "m02-master-template?open=일위대가조합", "m02-master-template?open=구조물조합", "m02-master-template?open=도면", ] - assert all("&root=master" in c[2] for c in cards[4:7]) + assert all("&root=master" in c[2] for c in cards[4:8]) @needs_node @@ -77,7 +78,7 @@ def test_해시_진입_셋(): + fn.group(0).replace("export function", "function") + "\nconsole.log(JSON.stringify(entryFromHash(null)));" ) - for want in ("구조물조합", "옵션", "도면", "일위대가조합"): + for want in ("구조물조합", "옵션", "도면", "일위대가조합", "단가산출"): got = _run(script, f"open={want}&root=master&company=3") assert got["open"] == want and got["root"] == "master" and got["company"] == "3" @@ -85,8 +86,12 @@ def test_해시_진입_셋(): def test_reveal_옵션은_M01_로(): side = _text(M02 / "M02_MasterTemplete_UI_Side.ts") body = side.split("const reveal = ")[1].split("\n };")[0] - # 716-1 · 716-3 — 옵션 정의는 M01 로 옮김 · M02 reveal 은 일위대가 · 도면만 펼침 - assert 'target === "일위대가조합" ? precomboSec : target === "도면" ? drawing : null' in body + # 716-1 · 716-3 · 717-2a — 옵션 정의는 M01 로 옮김 · M02 reveal 은 단가산출 · 일위대가 · 도면만 펼침 + flat = " ".join(body.split()) + assert ( + 'target === "단가산출" ? sangeunSec : target === "일위대가조합" ? precomboSec' + ' : target === "도면" ? drawing : null' + ) in flat assert 'sec?.classList.remove("is-collapsed");' in body assert "options" not in body and "옵션: section(" not in side page = _text(M02 / "M02_MasterTemplete_UI_Page.ts") diff --git a/resources/tester/test_717_2a_sangeun_list.py b/resources/tester/test_717_2a_sangeun_list.py new file mode 100644 index 000000000..ac19b209a --- /dev/null +++ b/resources/tester/test_717_2a_sangeun_list.py @@ -0,0 +1,54 @@ +"""M02 단가산출 칸 목록 줄(PLAN 717-2a · `M02_Labor_Ref.summary` sangeun 갈래) — 임시 storage. + +바탕 = 로직 「키 이름」 · 참조 = SK 키 · 없음 = None · 고친 줄 수 = 덮개 + 더한줄 + 뺀줄 · +쓰는 곳 수 = 그 SK 를 바탕 · 더한줄로 담은 문서 수 · 일위대가(precombo) 줄엔 안 붙음. +""" + +from M01_MasterData import M01_MasterData_Store as m01 +from M02_MasterTemplete import M02_Labor_Ref as refs +from M02_MasterTemplete import M02_MasterTemplete_Store as store +from M02_MasterTemplete.M02_Labor_Expand import Expand +from resources.tester.test_m02_labor_ref import MINE, world # noqa: F401 — 같은 임시 층 + +PCT = "GC001109" + + +def _sk(folder, **doc): + with store.at(folder): + return refs.create("sangeun", {"이름": "목록 시험", "단위": "㎡", "더한줄": [], **doc})[ + "이름" + ] + + +def _row(folder, kind, key): + return refs.summary(folder, kind, key, Expand(m01.loaded()[1])) + + +def test_sangeun_row_base_edits_users(world): + mine = world["personal"] + a = _sk(mine, 바탕={"로직": PCT}, 덮개={"labor:LB000036#0": {"qty": 0.5}}, 뺀줄=["pct:x#0"]) + name = m01.loaded()[1].logic(PCT)["이름"] + row = _row(mine, "sangeun", a) + assert row["바탕"] == f"{PCT} {name}".strip() + assert row["고친줄수"] == 2 and row["쓰는곳수"] == 0 + assert row["제목"] == "목록 시험" and row["단위"] == "㎡" + # 바탕 참조 SK · 더한줄 하나 → 그 SK 쓰는 곳 1 + b = _sk( + mine, + 바탕={"참조": {**MINE, "키": a}}, + 더한줄=[{"종류": "인력", "요소": "LB000036", "이름": "보통인부", "단위": "인", "수량": 1}], + ) + row_b = _row(mine, "sangeun", b) + assert row_b["바탕"] == a and row_b["고친줄수"] == 1 + assert _row(mine, "sangeun", a)["쓰는곳수"] == 1 + # 바탕 없음 + c = _sk(mine) + row_c = _row(mine, "sangeun", c) + assert row_c["바탕"] is None and row_c["고친줄수"] == 0 + + +def test_precombo_row_has_no_sangeun_fields(world): + with store.at(world["personal"]): + sc = refs.create("precombo", {"이름": "조합", "담은로직": []})["이름"] + row = _row(world["personal"], "precombo", sc) + assert not {"바탕", "고친줄수", "쓰는곳수"} & set(row) diff --git a/resources/tester/test_b01_settings_cards.py b/resources/tester/test_b01_settings_cards.py index ae486748a..d89b76154 100644 --- a/resources/tester/test_b01_settings_cards.py +++ b/resources/tester/test_b01_settings_cards.py @@ -97,22 +97,23 @@ WRITE_CO = READ_CO.replace("CompanyRead", "CompanyWrite") @pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") -def test_시스템_관리자_줄_칸별_여덟_고른_회사_해시(): - # 716-2 · 718-3 — 대시보드 설계 기본값 = 칸별 줄: M01 4(마스터 요소 · 단가산출 로직 · 구조물 옵션 정의 · - # 별칭 장부) + - # M02 3(일위대가 · 구조물 조합 · 도면 양식) + 계산 자리 +def test_시스템_관리자_줄_칸별_아홉_고른_회사_해시(): + # 716-2 · 718-3 · 717-2a — 대시보드 설계 기본값 = 칸별 줄: M01 4(마스터 요소 · 단가산출 로직 · + # 구조물 옵션 정의 · 별칭 장부) + + # M02 4(단가산출 · 일위대가 · 구조물 조합 · 도면 양식) + 계산 자리 assert _cards({"role": "SYSTEM_ADMIN", "is_master": False}, 3) == [ ["M01_Title", READ_SRC, M01], ["B01_Dashboard_CardLogic", READ_SRC, f"{M01}?open=로직"], ["B01_Dashboard_CardStructOptionDef", WRITE_M, f"{M01}?open=옵션"], ["B01_Dashboard_CardAliasLedger", WRITE_M, f"{M01}?open=별칭"], + ["B01_Dashboard_CardSangeun", WRITE_M, f"{M02}?open=단가산출&root=master&company=3"], ["B01_Dashboard_CardPrecombo", WRITE_M, f"{M02}?open=일위대가조합&root=master&company=3"], ["B01_Dashboard_CardStructCombo", WRITE_M, f"{M02}?open=구조물조합&root=master&company=3"], ["B01_Dashboard_CardDrawing", WRITE_M, f"{M02}?open=도면&root=master&company=3"], CALC_SITES, ] # 회사가 없으면 해시에 안 실음 - assert _cards({"role": "SYSTEM_ADMIN", "is_master": False})[5][2] == ( + assert _cards({"role": "SYSTEM_ADMIN", "is_master": False})[6][2] == ( f"{M02}?open=구조물조합&root=master" ) @@ -243,7 +244,8 @@ def test_구조물_조합_기본_층_개인(): 134-4 — 층 없는 주소도 개인(회사 없으면 마스터) · 사전조합 기본 층도 같은 규칙.""" combo = f"{M02}?open=구조물조합&root=personal" rows = _cards({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": 1}, 3) - assert rows[4:7] == [ + assert rows[4:8] == [ + ["B01_Dashboard_CardSangeun", WRITE_M, f"{M02}?open=단가산출&root=personal&company=3"], ["B01_Dashboard_CardPrecombo", WRITE_M, f"{M02}?open=일위대가조합&root=personal&company=3"], ["B01_Dashboard_CardStructCombo", WRITE_M, f"{combo}&company=3"], ["B01_Dashboard_CardDrawing", WRITE_M, f"{M02}?open=도면&root=master&company=3"], @@ -254,7 +256,7 @@ def test_구조물_조합_기본_층_개인(): assert _cards({"role": "USER", "is_master": True, "company_id": 1})[0][2] == combo assert _cards({"role": "USER", "is_master": False, "company_id": 1})[0][2] == combo # 개인 층 없음(회사 없음) = 지금 규칙 - assert _cards({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": None})[5][2] == ( + assert _cards({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": None})[6][2] == ( f"{M02}?open=구조물조합&root=master" ) assert _cards({"role": "ADMIN", "is_master": False, "company_id": None})[0][2] == ( diff --git a/resources/tester/test_m02_hopyo_screen.py b/resources/tester/test_m02_hopyo_screen.py index 892adfe59..4bc8efb8f 100644 --- a/resources/tester/test_m02_hopyo_screen.py +++ b/resources/tester/test_m02_hopyo_screen.py @@ -308,7 +308,7 @@ def test_옛_즉시_확정과_바로_만들기_없음(): users = sorted(name for name, text in texts.items() if "buildHopyo(" in text) assert users == ["Hopyo_Check", "Hopyo_Sheet"] # 확인 창 · 엑셀 보기 [저장] 물음 뒤 check = texts["Hopyo_Check"] - assert '"사전조합 생성"' in check and "맞음 · 별칭 일괄 체크" in check + assert '`${L("M02_PrecomboTitle")} 생성`' in check and "맞음 · 별칭 일괄 체크" in check assert "확인하고" not in check # 359-4 명사형 assert "showLoadingOverlay()" in check and "dropDraft(k)" in check assert "m02-hopyo-check:" in check and "POLL_MS" in check diff --git a/resources/tester/test_m02_layer_screen.py b/resources/tester/test_m02_layer_screen.py index 4dff150c1..a74df5040 100644 --- a/resources/tester/test_m02_layer_screen.py +++ b/resources/tester/test_m02_layer_screen.py @@ -89,7 +89,8 @@ def test_주소_인자를_라우트에서_뗌(): assert ( 'raw === "master" || raw === "company" || raw === "personal"' in layer ) # 없음 = 기본(134-4) - assert '["구조물조합", "옵션", "도면", "일위대가조합"]' in layer # 일위대가 조합 서고 = 50-3 + # 일위대가 조합 서고 = 50-3 · 단가산출 = 717-2a + assert '["구조물조합", "옵션", "도면", "일위대가조합", "단가산출"]' in layer picker = _text(ROOT / "ui_template" / "ui_template_item_picker.ts") assert 'open: "구조물조합", root: "personal"' in picker and "layer=personal" not in picker diff --git a/resources/tester/test_m02_precombo_scope.py b/resources/tester/test_m02_precombo_scope.py index 1271ecae6..921f89074 100644 --- a/resources/tester/test_m02_precombo_scope.py +++ b/resources/tester/test_m02_precombo_scope.py @@ -129,7 +129,7 @@ def test_분류_고정_폴더_지움_자유_폴더_메뉴_끌어_옮기기(): def test_목록표_빈_구분_미분류_고르기_창_새_분류_거름(): """149-2 — 목록표 빈 구분 = 미분류 · 고르기 창 거름 = 품셈 구분 + 사전조합 새 분류""" lib = _text("M02_MasterTemplete_Library_List.ts") - assert 'row.구분 || tc("Lib_Unsorted"),' in lib + assert '(r) => r.구분 || tc("Lib_Unsorted")],' in lib # 717-2a 열 표(COLUMNS) pick = _text("M02_MasterTemplete_Labor_Pick.ts") assert "new Map([...subs, ...classes].map((s) => [s.name, new Set(s.details)]))" in pick assert "classes = precomboClasses(def);" in pick @@ -143,6 +143,7 @@ def test_왼쪽_칸_차례_일위대가_구조물조합_구분선_불러오기_7 side = _text("M02_MasterTemplete_UI_Side.ts") order = side[side.index("m02-side__lists") :] marks = [ + "sangeunSec,", # 717-2a 맨 위 "precomboSec,", "summary,", "createDivider()", diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index 6ae515608..cfc15a584 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -191,4 +191,20 @@ export const ui_locales_m2 = { M02_HopyoImport: ["일위대가 불러오기", "Import unit-price sheets"], B01_Dashboard_CardPrecombo: ["일위대가", "Unit-cost items"], M02_PrecomboTitle: ["일위대가", "Unit-cost items"], + B01_Dashboard_CardSangeun: ["단가산출", "Unit-price analyses"], + M02_SangeunTitle: ["단가산출", "Unit-price analyses"], + M02_SangeunNone: ["단가산출 없음", "No unit-price analyses"], + M02_SangeunNewHere: ["여기에 새 단가산출", "New unit-price analysis here"], + M02_SangeunBase: ["바탕", "Base"], + M02_SangeunNoBase: ["바탕 없음", "No base"], + M02_SangeunEdited: ["고친 줄", "Edited rows"], + M02_SangeunUsers: ["쓰는 곳", "Used by"], + M02_SangeunCover: ["덮개", "Overrides"], + M02_SangeunAdded: ["더한 줄", "Added rows"], + M02_SangeunRemoved: ["뺀 줄", "Removed rows"], + M02_SangeunOpened: ["열린 입력", "Open inputs"], + M02_SangeunDetailSoon: [ + "단가산출 상세 표 · 고침 — 준비 중(717-2b)", + "Unit-price detail table and editing — coming next (717-2b)", + ], } as const;