diff --git a/B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts b/B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts index fc8e13020..dd0322a7b 100644 --- a/B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts +++ b/B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts @@ -8,7 +8,7 @@ * 고친 칸 = 칸 강조(셈 줄 `auto_{칸}` 있음) · 풍선 = 자동 값 * 줄 누름 = 단가를 낸 탭 줄(자재 → 재료비목록표 · 구조물 → 일위대가목록표) * 레벨 = 줄 `level`(셈이 냄) → 공용 표 level 옵션(공종 · 명칭 칸 들여쓰기 · 201-1) · 다루기 = `…_Bill_Level` - * 「연결 ▾」 칸 = 항목 줄(자재 · 조달수수료 빼고) · 누름 = 우클릭 [연결] 과 같은 메뉴 + * 「단가연결 ▾」 칸 = 항목 줄(자재 · 조달수수료 빼고) · 누름 = 우클릭 [연결] 과 같은 메뉴 * 접히지 않는 나무(235-1) = 폴더(묶음 · 자식 있는 줄) = 이름만 · 수량 · 단가 빈칸 · 금액 = 자손 합 수식(읽기 전용) · * 파일(항목) = 이름 · 규격 · 단위 · 수량 + 연결 · 안내선 · 아이콘 = 공용 표 level.tree · 고칠 칸 = 더블클릭 입력 * ========================================================================== */ @@ -37,7 +37,7 @@ export const buildBill: TabBuild = (ctx): BillTable => { const rows: DataRow[] = raw.map((r: Row) => ({ ...r, name: r.no ? `${r.no} ${r.name}` : r.name, - connect: linkable(r) ? `${!r.auto && r.note ? r.note : r.ref ? "연결됨" : "연결"} ▾` : "", + connect: linkable(r) ? `${!r.auto && r.note ? r.note : r.ref ? "연결됨" : "단가연결"} ▾` : "", })); const direct: Row = bill.totals?.direct || {}; const pair = (key: string, label: string) => [ @@ -57,14 +57,14 @@ export const buildBill: TabBuild = (ctx): BillTable => { ...pair("labor", "노무비"), ...pair("material", "재료비"), ...pair("expense", "경비"), - text("connect", "연결", 96), + text("connect", "단가연결", 96), text("note", "비고", 200), ], rows, total: { name: "순공사비 계(관급 제외)", ...direct }, frozen: 1, hint: - "줄을 우클릭하면 항목 추가 · 줄 삭제 · 옮김 · 「연결 ▾」로 일위대가 · 단가산출 연결" + + "줄을 우클릭하면 항목 추가 · 줄 삭제 · 옮김 · 「단가연결 ▾」로 일위대가 · 단가산출 연결" + (direct.blank ? ` · 빈 줄 ${direct.blank} — 계는 아는 금액만 · 원가계산서는 빈 줄이 없을 때만 받음` : ""), diff --git a/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Edit.ts b/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Edit.ts index f036c2206..35a5dcb81 100644 --- a/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Edit.ts +++ b/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Edit.ts @@ -252,8 +252,8 @@ export function billMenu(ctx: TabContext, rows: DataRow[], i: number): MapContex else list.splice(at < 0 ? list.length : at + 1, 0, newItem(e.parent)); }); if (linkable(row)) { - const [picks, more] = linkFor(ctx, rows, i); - parts[1].push({ label: "연결", children: picks }, ...more); + const [picks, more] = linkFor(ctx, rows, i); // 251-6 단가연결 = 모달 하나(옛 하위 메뉴 다섯) + parts[1].push(...picks, ...more); } // 옮김 · 레벨 — 같은 부모 형제와 자손째 if (!fixed(key)) { @@ -355,7 +355,8 @@ export function openLinkMenu(ctx: TabContext, rows: DataRow[], i: number): void const box = lastCell?.getBoundingClientRect(); if (!shared || !box || !linkable(rows[i])) return; const [picks, more] = linkFor(ctx, rows, i); - shared.open(box.left, box.bottom, more.length ? [...picks, "-", ...more] : picks); + if (!more.length) return picks[0][1](); // 251-6 아직 연결 없음 = 단가연결 모달 바로 + shared.open(box.left, box.bottom, [...picks, "-", ...more]); } /** B09 메뉴 한 벌(우클릭 · 183-3 칸 고르기 같이 씀) — 화면 기준 자리 */ diff --git a/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Link.ts b/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Link.ts index 9152d1120..c8293beb6 100644 --- a/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Link.ts +++ b/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Link.ts @@ -4,6 +4,8 @@ * 서고(마스터 · 회사 · 개인) 구조물조합 · 일위대가 사전조합(204-2 — 서버가 프로젝트 사본으로 들여와 새 * 대가 호표 · 같은 것 = 있는 사본) · M01 로직(새 산근 = 서버 셈 · [저장] 뒤 단가) · 로직 입력값 고치기 · * 연결 풀기. + * 251 단가연결 = 한 모달(`openPriceLink` · 종류 고르기 + 검색) — B09 설계내역서 · B08 거리이정표 같이 씀 · + * 단가산출 = 옵션 값 창(`askInputs` · 160) * * 줄에 적는 일 = 부르는 쪽 `apply(ref, from)`(`…_UI_Tabs_Bill_Edit` · 저장 꼴 모름) * 로직 · 사전조합 고르기 · 입력값 = M02 창 재사용 · 구조물조합 고르기 = B08 [선택] 나무(173) 재사용 · @@ -12,9 +14,8 @@ import { API_BASE_URL, CURRENT_PROJECT_ID_KEY } from "@config/config_frontend"; import { showToast } from "@ui/ui_template_elements"; -import { createFindList } from "@ui/ui_template_find_list"; -import { openItemPicker } from "@ui/ui_template_item_picker"; -import { openModal } from "@ui/ui_template_modal"; +import { loadRows } from "@ui/ui_template_item_picker"; +import { itemName, type PickerLayer } from "@ui/ui_template_item_picker_data"; import { fetchLogicInputs } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Api"; import { openLaborInput } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Input"; import { openLogicPicker } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Pick"; @@ -24,47 +25,14 @@ import type { TabContext } from "./B09_Estimation_UI_Tabs"; /** 연결 적용 — ref = null 이면 풀기 · from = 고른 호표(명칭 · 규격 · 단위 채움) */ export type ApplyRef = (ref: Row | null, from?: Row) => void; export type Leaf = [string, () => void]; - -/** 있는 호표 고르기 창 — 일위대가목록표(dg) · 단가산출근거목록표(sg) */ -function pickHopyo(ctx: TabContext, apply: ApplyRef, tab: "dg" | "sg"): void { - const listing: Row[] = (tab === "dg" ? ctx.sheets.daega : ctx.sheets.sg_list) || []; - if (!listing.length) return showToast("고를 호표 없음 — 연결된 구조물 조합 사본이 없음", "error"); - const label = (r: Row): string => (tab === "dg" ? `대가${r.no}` : `산근 ${r.no}호표`); - openModal({ - title: tab === "dg" ? "일위대가 호표 고르기" : "단가산출 호표 고르기", - closeLabel: "닫기", - mount: (body, close) => { - const finder = createFindList({ placeholder: "호표 · 명칭 · 규격 찾기" }); - const find = (): void => { - const q = finder.search.value.trim().toLowerCase(); - const found = listing.filter( - (r) => !q || `${label(r)} ${r.name} ${r.spec}`.toLowerCase().includes(q), - ); - finder.hint.textContent = `${found.length} / ${listing.length}`; - finder.setRows( - found.map((r) => ({ - key: String(r.key), - tag: label(r), - name: String(r.name || ""), - sub: String(r.spec || ""), - unit: String(r.unit || ""), - onPick: () => { - close(); - apply({ tab, key: r.key }, r); - }, - })), - ); - }; - finder.search.addEventListener("input", find); - find(); - body.append(finder.root); - requestAnimationFrame(() => finder.search.focus()); - }, - }); -} +const LAYER_TEXT: Record = { + system: "마스터", + company: "회사", + personal: "개인", +}; /** M01 로직 → 입력값 창 → 연결(새 산근 · 단가는 [저장] 뒤 서버 셈) */ -function askInputs(apply: ApplyRef, code: string, name: string, old?: Row): void { +export function askInputs(apply: ApplyRef, code: string, name: string, old?: Row): void { fetchLogicInputs([code]).then( (got) => { const logic = got.로직[0] ?? null; @@ -94,10 +62,10 @@ function askInputs(apply: ApplyRef, code: string, name: string, old?: Row): void /** 서고 것 → 프로젝트 사본 · 새 대가 호표(204-2) — 서버가 들여옴 → 받은 원가 자료(대가 블록 · 산근)만 * 바꿔 끼우고(사용자 캐시 그대로) 줄 연결 `{tab: dg, key: 블록 id, lib: 사본 폴더}` */ async function importLib( - ctx: TabContext, - apply: ApplyRef, + o: { apply: ApplyRef; onPayload?: (payload: Row) => void }, pick: { kind: "combo" | "precombo"; layer: string; owner: string | null; key: string }, ): Promise { + const apply = o.apply; const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY); if (!projectId) return; showToast("가져오는 중…", "info"); @@ -115,7 +83,7 @@ async function importLib( } if (!got.key) throw new Error("사본은 들여왔으나 호표를 못 만듦"); const { user: _user, ...rest } = got.payload as Row; - Object.assign(ctx.payload, rest); + o.onPayload?.(rest); const from = ((rest.sheets?.daega as Row[]) || []).find((r) => r.key === got.key); apply({ tab: "dg", key: got.key, lib: got.폴더 }, from); showToast( @@ -127,39 +95,79 @@ async function importLib( } } -/** 서고 구조물조합 — B08 [선택] 나무(마스터 · 회사 · 개인 · 보기만) */ -function pickCombo(ctx: TabContext, apply: ApplyRef): void { - void openItemPicker({ current: null, canCopy: false }).then((got) => { - if (got.action !== "apply") return; - const { layer, owner, key } = got.item; - void importLib(ctx, apply, { kind: "combo", layer, owner, key }); - }); -} - -/** 서고 일위대가 사전조합 — M02 고르기 창 서고 줄만(층 단추 마스터 · 회사 · 개인) */ -function pickPrecombo(ctx: TabContext, apply: ApplyRef): void { - openLogicPicker(() => undefined, { - layer: "personal", - owner: null, - libOnly: true, - title: "일위대가 사전조합 고르기", - pick: (ref) => - void importLib(ctx, apply, { kind: "precombo", layer: ref.층, owner: ref.주인, key: ref.키 }), - }); -} - -/** 항목 줄 연결 메뉴 — [고르기 셋, 그 밖(로직 입력값 · 연결 풀기)] · ref = 지금 연결 */ +/** 항목 줄 단가연결 메뉴(251-6) — [단가연결 모달, 그 밖(로직 입력값 · 단가연결 풀기)] · ref = 지금 연결 */ export function linkItems(ctx: TabContext, ref: Row | null, apply: ApplyRef): [Leaf[], Leaf[]] { const more: Leaf[] = []; if (ref?.tab === "logic") more.push(["로직 입력값 고치기", () => askInputs(apply, ref.code, ref.name || "", ref)]); - if (ref) more.push(["연결 풀기", () => apply(null)]); - const picks: Leaf[] = [ - ["일위대가 호표", () => pickHopyo(ctx, apply, "dg")], - ["단가산출 호표", () => pickHopyo(ctx, apply, "sg")], - ["구조물 조합", () => pickCombo(ctx, apply)], - ["일위대가 사전조합", () => pickPrecombo(ctx, apply)], - ["단가산출", () => openLogicPicker((logic) => askInputs(apply, logic.키, logic.이름))], + if (ref) more.push(["단가연결 풀기", () => apply(null)]); + const open: Leaf = [ + "단가연결", + () => + openPriceLink({ + apply, + hopyo: { dg: ctx.sheets.daega || [], sg: ctx.sheets.sg_list || [] }, + onPayload: (p) => Object.assign(ctx.payload, p), + }), ]; - return [picks, more]; + return [[open], more]; +} + +/** 단가연결 모달(251 · 159 · 160) — 한 목록에 단가산출 로직 · 일위대가 사전조합 · 구조물 조합(마스터 · 회사 · + * 개인 층 단추) · 호표(B09 · 있을 때) · 종류 고르기 + 검색 + 구분 · 상세구분 거름 · + * 단가산출 = 옵션 값 창(`askInputs` · 160) · 사전조합 · 구조물 조합 = 프로젝트 사본 들여옴 → `{tab: dg, key, lib}` */ +export interface PriceLinkOpts { + apply: ApplyRef; + /** B09 호표 목록(일위대가 · 단가산출) — 없으면 그 종류 없음(B08) */ + hopyo?: { dg: Row[]; sg: Row[] }; + /** 서고 사본 들여온 뒤 받은 원가 자료(B09 = 화면 자료에 끼움) */ + onPayload?: (payload: Row) => void; +} + +export async function openPriceLink(o: PriceLinkOpts): Promise { + const hop = (tab: "dg" | "sg") => + ((tab === "dg" ? o.hopyo?.dg : o.hopyo?.sg) ?? []).map((r) => ({ + kind: tab, + key: String(r.key), + tag: tab === "dg" ? `대가${r.no}` : `산근 ${r.no}호표`, + name: String(r.name || ""), + sub: String(r.spec || ""), + unit: String(r.unit || ""), + onPick: () => o.apply({ tab, key: r.key }, r), + })); + // 구조물 조합 = 마스터 · 회사 · 개인 한 목록(B08 [선택] 나무와 같은 원천) + const combos = (await loadRows().catch(() => [])).map((r) => ({ + kind: "combo", + key: `${r.layer}:${r.owner ?? ""}:${r.key}`, + tag: "구조물 조합", + name: itemName(r), + sub: `${LAYER_TEXT[r.layer ?? "system"]} · ${r.key}`, + unit: String(r.단위 || ""), + onPick: () => + void importLib(o, { + kind: "combo", + layer: r.layer ?? "system", + owner: r.owner ?? null, + key: r.key, + }), + })); + openLogicPicker((logic) => askInputs(o.apply, logic.키, logic.이름), { + layer: "personal", + owner: null, + title: "단가연결", + extra: [...combos, ...hop("dg"), ...hop("sg")], + kindFilter: [ + { value: "logic", text: "단가산출" }, + { value: "precombo", text: "일위대가 사전조합" }, + { value: "combo", text: "구조물 조합" }, + ...(o.hopyo + ? [ + { value: "dg", text: "일위대가 호표" }, + { value: "sg", text: "단가산출 호표" }, + ] + : []), + ], + pick: (ref) => + void importLib(o, { kind: "precombo", layer: ref.층, owner: ref.주인, key: ref.키 }), + }); } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts index 310825c63..043ece45b 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts @@ -3,6 +3,8 @@ * [로직 더하기] 창 — 한 목록에 구분(단가산출 · 단가승격 · 일위조합) 칸 · 치는 대로 걸러짐(찾기 단추 없음) · * 단가승격 · 일위조합 줄은 층 단추(위 층만 · 짝 `M02_Labor_Ref.readable`) 층의 서고 · 고르면 참조 {층 · 주인 · 키} · * 프로젝트 · 카드엔 서고 줄 없음 · 거름 = 로직 품셈 분류 + 사전조합 새 분류(149-2 · 구조물군 › 공통 + 종류) + * 단가연결(251 · B08 거리이정표 · B09 설계내역서) = 종류 고르기(단가산출 · 사전조합 · 덧붙인 줄 종류) + + * 덧붙일 줄(구조물 조합 · B09 호표) — `lib.extra` · `lib.kindFilter` * ========================================================================== */ import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements"; @@ -38,6 +40,18 @@ export interface LibraryPick { /** 서고 줄만(로직 목록 안 받음 · B09 설계내역서 연결 204-2) · 창 제목 */ libOnly?: boolean; title?: string; + /** 목록 끝 덧붙일 줄(251 · 구조물 조합 · B09 호표) — kind = 종류 고르기 값 */ + extra?: Array<{ + kind: string; + key: string; + tag: string; + name: string; + sub: string; + unit: string; + onPick: () => void; + }>; + /** 종류 고르기 칸(251 · 값 logic · precombo · 덧붙인 줄 kind) */ + kindFilter?: Array<{ value: string; text: string }>; } /** 고를 수 있는 층 — 위 층만(마스터 → 마스터 · 회사 → 그 회사 · 마스터 · 개인 → 그 개인 · 그 회사 · 마스터) */ @@ -72,12 +86,21 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra let scopes: Scope[] = []; let scope: Scope | null = null; let libRows: { kind: "Kind_Combo"; row: LibraryRow }[] = []; + // 251 종류 고르기 — 값 "" = 전체 · logic · precombo · 덧붙인 줄 kind + const kindSel = lib?.kindFilter + ? createSelectField({ + options: [{ value: "", text: L("M01_All") }, ...lib.kindFilter], + compact: true, + label: "종류", + }) + : null; + const kindOk = (k: string): boolean => !kindSel?.select.value || kindSel.select.value === k; let loading = false; let seq = 0; /* 공용 찾기 목록 — 구분 · 상세구분 2열 · 이름 왼쪽 맞춤 · ↑ ↓ Enter */ const finder = createFindList({ placeholder: tc("Pick_Search"), - filters: [sub.root, detail.root], + filters: [...(kindSel ? [kindSel.root] : []), sub.root, detail.root], extra: [layerBar], }); @@ -85,18 +108,26 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra const q = finder.search.value.trim().toLowerCase(); const found = all.filter( (x) => + kindOk("logic") && (!sub.select.value || x.구분 === sub.select.value) && (!detail.select.value || x.상세구분 === detail.select.value) && matchLogic(finder.search.value, x).ok, // 이름 · 키 · 별칭 …(공용 · 208-1) ); const libFound = libRows.filter( (x) => + kindOk("precombo") && (!sub.select.value || x.row.구분 === sub.select.value) && (!detail.select.value || x.row.상세구분 === detail.select.value) && (!q || `${x.row.이름} ${x.row.제목 ?? ""}`.toLowerCase().includes(q)), ); - const total = all.length + libRows.length; - const shown = found.length + libFound.length; + const extra = (lib?.extra ?? []).filter( + (x) => + kindOk(x.kind) && + !sub.select.value && + (!q || `${x.tag} ${x.name} ${x.sub}`.toLowerCase().includes(q)), + ); + const total = all.length + libRows.length + (lib?.extra?.length ?? 0); + const shown = found.length + libFound.length + extra.length; finder.hint.textContent = loading ? tc("Loading") : shown @@ -132,6 +163,13 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra }, }; }), + ...extra.map((x) => ({ + ...x, + onPick: () => { + close(); + x.onPick(); + }, + })), ]); }; const loadLib = async (): Promise => { @@ -192,6 +230,7 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra find(); }); detail.select.addEventListener("change", find); + kindSel?.select.addEventListener("change", find); finder.search.addEventListener("input", find); layerBar.style.display = "none"; diff --git a/resources/tester/test_166_2_b09_bill_hand.py b/resources/tester/test_166_2_b09_bill_hand.py index 8941dc397..436f12882 100644 --- a/resources/tester/test_166_2_b09_bill_hand.py +++ b/resources/tester/test_166_2_b09_bill_hand.py @@ -227,10 +227,10 @@ console.log(JSON.stringify(out)); out = json.loads(_node(src + body)) # 246-1 더함 = 「항목 추가」 하나 · 지움 = 「줄 삭제」 · 2단 제목 ← = 1단으로 assert out["title"] == ["항목 추가", "-", "내어쓰기 ←", "-", "줄 삭제"] - assert out["auto"] == ["항목 추가", "-", "연결", "-", "아래로 ↓", "내어쓰기 ←", + assert out["auto"] == ["항목 추가", "-", "단가연결", "-", "아래로 ↓", "내어쓰기 ←", "-", "자동값으로 되돌리기", "줄 삭제"] # fmt: skip # 204 — 위 항목 아래로 들여쓰기 됨(그 항목 = 저절로 묶음) - assert out["hand"] == ["항목 추가", "-", "연결", "연결 풀기", "-", "위로 ↑", + assert out["hand"] == ["항목 추가", "-", "단가연결", "단가연결 풀기", "-", "위로 ↑", "들여쓰기 →", "내어쓰기 ←", "묶음 줄로 바꾸기", "-", "줄 삭제"] # fmt: skip # 항목 추가(항목 줄) = 바로 아래 같은 부모 · 옛 꼴 → v2(옛 손 줄 칸 · 수량 덮개 옮김) below = out["below"] diff --git a/resources/tester/test_182_b09_bill_level.py b/resources/tester/test_182_b09_bill_level.py index 1ae488159..122959fd6 100644 --- a/resources/tester/test_182_b09_bill_level.py +++ b/resources/tester/test_182_b09_bill_level.py @@ -266,6 +266,13 @@ globalThis.openModal = (o) => { picked.title = o.title; o.mount({ append() {} }, () => (picked.closed = true)); }; +// 251-6 단가연결 = 한 모달(openLogicPicker 바탕) — 호표 줄 = lib.extra +globalThis.loadRows = async () => []; +globalThis.openLogicPicker = (done, lib) => { + picked.title = lib.title; + picked.kinds = lib.kindFilter.map((k) => k.text); + picked.rows = lib.extra; +}; let user = __USER__; const ctx = { sheets: { bill: __BILL__, daega: __DAEGA__ }, payload: {}, get user() { return user; }, setUser: (u) => (user = u) }; @@ -288,11 +295,11 @@ const out = { table.onCell(at("hand:a"), "connect"); table.onCell(at("hand:g1"), "connect"); table.onCell(at("earth:4"), "connect"); -table.onCell(at("hand:c"), "connect"); +table.onCell(at("hand:c"), "connect"); // 연결 없음 = 단가연결 창 바로(메뉴 없음) +await new Promise((r) => setTimeout(r, 0)); out.menus = opened.map((m) => [m.x, m.y, m.items.map(name)]); -opened[2].items[0][1](); // hand:c → 일위대가 호표 고르기 창 -picked.rows[0].onPick(); -out.picked = [picked.title, picked.closed, picked.rows.map((r) => r.tag)]; +picked.rows.find((r) => r.tag === "대가3").onPick(); +out.picked = [picked.title, picked.kinds, picked.rows.map((r) => r.tag)]; out.ref = user.bill.rows.find((r) => r.key === "hand:c").ref; console.log(JSON.stringify(out)); """ @@ -318,17 +325,12 @@ def test_연결_단추_칸_메뉴_고르기_창_TS(): ] # fmt: skip 201-1 레벨 = 공용 표 level assert out["lv"] == ["name", 1, 3, 4, 3] assert out["edit"] == [True, False] # 묶음 줄 = 이름만 - picks = [ - "일위대가 호표", - "단가산출 호표", - "구조물 조합", - "일위대가 사전조합", - "단가산출", - ] # 204-2 · 246-1 이름 셋 - # 누른 칸 아래(left · bottom) · 묶음 줄 = 안 엶 · 연결된 줄(자동 줄도) = 연결 풀기 더함 - unlink = [10, 40, [*picks, "-", "연결 풀기"]] - assert out["menus"] == [unlink, unlink, [10, 40, picks]] - assert out["picked"] == ["일위대가 호표 고르기", True, ["대가3"]] # 166-2 고르기 창 재사용 + # 251-6 누른 칸 아래(left · bottom) · 묶음 줄 = 안 엶 · 연결된 줄 = [단가연결 · 단가연결 풀기] · + # 연결 없는 줄 = 단가연결 창 바로(종류 = 단가산출 · 사전조합 · 구조물 조합 · 호표 둘) + unlink = [10, 40, ["단가연결", "-", "단가연결 풀기"]] + assert out["menus"] == [unlink, unlink] + kinds = ["단가산출", "일위대가 사전조합", "구조물 조합", "일위대가 호표", "단가산출 호표"] + assert out["picked"] == ["단가연결", kinds, ["대가3"]] assert out["ref"] == DG diff --git a/resources/tester/test_184_3_b09_bill_menu.py b/resources/tester/test_184_3_b09_bill_menu.py index e7e72e92b..b3daf1fba 100644 --- a/resources/tester/test_184_3_b09_bill_menu.py +++ b/resources/tester/test_184_3_b09_bill_menu.py @@ -123,15 +123,15 @@ def test_메뉴_모든_줄(): # 맨 위 제목 = 항목 추가(첫 하위) · 아래로(자식 있는 맨 위 = 못 지움 · 맨 위 = 내어쓰기 없음) assert out["sec:1"] == [*add, "-", "아래로 ↓"] # 자동 항목 = 더함 · 연결 · 옮김 · 삭제(고친 칸 없음 = 자동값으로 없음) - assert out["earth:4"] == [*add, "-", "연결", "연결 풀기", "-", + assert out["earth:4"] == [*add, "-", "단가연결", "단가연결 풀기", "-", "아래로 ↓", "내어쓰기 ←", "-", "줄 삭제"] # fmt: skip # 204 — 위 형제가 항목이어도 들여쓰기 됨(그 항목 = 저절로 묶음) - assert out["earth:5"] == [*add, "-", "연결", "-", "위로 ↑", "아래로 ↓", + assert out["earth:5"] == [*add, "-", "단가연결", "-", "위로 ↑", "아래로 ↓", "들여쓰기 →", "내어쓰기 ←", "-", "자동값으로 되돌리기", "줄 삭제"] # fmt: skip assert out["hand:g"] == [*add, "-", "위로 ↑", "아래로 ↓", "들여쓰기 →", "내어쓰기 ←", "항목으로 바꾸기", "-", "줄 삭제"] # fmt: skip - assert out["hand:a"] == [*add, "-", "연결", "연결 풀기", "-", "내어쓰기 ←", + assert out["hand:a"] == [*add, "-", "단가연결", "단가연결 풀기", "-", "내어쓰기 ←", "묶음 줄로 바꾸기", "-", "줄 삭제"] # fmt: skip # 조달수수료 · 관급 자재 = 연결 · 옮김 · 삭제 없음 · 관급자재대 제목 = 옮김 · 삭제 없음 · # 자재 줄 = 1단으로 못 냄(별도 자재 mat:M1 도) @@ -174,7 +174,7 @@ def test_삭제_옮김_새_공종_자동값으로(): ("mat:M2", "내어쓰기 ←"), ("earth:5", "자동값으로 되돌리기"), ("hand:b", "들여쓰기 →"), ("hand:a", "내어쓰기 ←"), ("sec:1.2", "항목 추가"), ("earth:4", "항목 추가"), ("hand:a", "묶음 줄로 바꾸기"), ("hand:g", "항목으로 바꾸기"), - ("earth:4", "연결 풀기"), ("earth:4", "내어쓰기 ←"), ("fee", "항목 추가")] # fmt: skip + ("earth:4", "단가연결 풀기"), ("earth:4", "내어쓰기 ←"), ("fee", "항목 추가")] # fmt: skip out = _menu(user, runs=runs)["runs"] keys = lambda b: [k for k, _ in _order(b)] # noqa: E731 # 덮개에 없는 보이는 줄(hand:b · 새 자동 줄) = 보이는 앞 줄 뒤에 굳힘 @@ -215,7 +215,7 @@ def test_삭제_옮김_새_공종_자동값으로(): got = {e["key"]: e for e in out["hand:g|항목으로 바꾸기"]["rows"]} assert "kind" not in got["hand:g"] and got["hand:a"]["parent"] == "sec:1.2" # 자동 줄 연결 풀기 = ref 덮개 null - assert {e["key"]: e for e in out["earth:4|연결 풀기"]["rows"]}["earth:4"]["ref"] is None + assert {e["key"]: e for e in out["earth:4|단가연결 풀기"]["rows"]}["earth:4"]["ref"] is None # 조달수수료 항목 추가 = 관급자재대 마지막 자식(조달수수료는 셈이 끝에 둠) got = out["fee|항목 추가"]["rows"] assert (got[-1]["name"], got[-1]["parent"], got[-2]["key"]) == ("새 줄", "sec:G", "mat:M2") @@ -270,7 +270,7 @@ def test_표_칸_강조_번호_연결_칸(): ) out = json.loads(_node(_ts([*SRC, "UI_Tabs_Bill"]) + body)) assert out["names"] == ["1 토공사", "1.1 절토", "earth:4"] # 번호 = 셈 no - assert out["connect"] == ["연결됨 ▾", "연결 ▾", "대가3 ▾", "", "", ""] + assert out["connect"] == ["연결됨 ▾", "단가연결 ▾", "대가3 ▾", "", "", ""] assert out["cls"] == ["is-edited", None, None] assert out["tip"] == "자동 값 5 — 우클릭 [자동값으로 되돌리기]" assert out["edit"] == [True, False, True, True, False] diff --git a/resources/tester/test_204_2_b09_bill_library.py b/resources/tester/test_204_2_b09_bill_library.py index 4da59ffb2..71cd98c8c 100644 --- a/resources/tester/test_204_2_b09_bill_library.py +++ b/resources/tester/test_204_2_b09_bill_library.py @@ -188,10 +188,11 @@ globalThis.showToast = (m, k) => toasts.push([k, m]); globalThis.API_BASE_URL = "/api"; globalThis.CURRENT_PROJECT_ID_KEY = "pid"; globalThis.localStorage = { getItem: () => "P1" }; -globalThis.openLogicPicker = (done, lib) => { sent.push(["pre", lib.layer, lib.libOnly, lib.title]); - lib.pick({ 층: "company", 주인: "7", 키: "SC-000002" }, "회사 조합"); }; -globalThis.openItemPicker = async () => ({ action: "apply", - item: { layer: "system", owner: null, key: "SS000011", name: "관매설" } }); +let picker = null; // 251-6 단가연결 = 한 창(openLogicPicker · 사전조합 + 덧붙인 줄 구조물 조합) +globalThis.openLogicPicker = (done, lib) => { picker = lib; + sent.push(["pre", lib.layer, lib.kindFilter.map((k) => k.value), lib.title]); }; +globalThis.loadRows = async () => [{ key: "SS000011", layer: "system", owner: null, 단위: "m" }]; +globalThis.itemName = () => "관매설"; const answer = { 폴더: "precombo@company@7@SC-000002", 만듦: true, key: "DG:new", payload: { user: { bill: "서버 것" }, inputs: { daega: { libs: {} } }, sheets: { daega: [{ key: "DG:new", name: "회사 조합", spec: "", unit: "㎡" }] } } }; @@ -202,9 +203,12 @@ const applied = []; const apply = (ref, from) => applied.push([ref, from && from.name]); const [picks, more] = linkItems(ctx, { tab: "dg", key: "DG:a" }, apply); const out = { picks: picks.map((x) => x[0]), more: more.map((x) => x[0]) }; -picks.find((x) => x[0] === "일위대가 사전조합")[1](); +picks[0][1](); await new Promise((r) => setTimeout(r, 0)); -picks.find((x) => x[0] === "구조물 조합")[1](); +out.combo = picker.extra.filter((x) => x.kind === "combo").map((x) => [x.tag, x.name, x.sub]); +picker.pick({ 층: "company", 주인: "7", 키: "SC-000002" }, "회사 조합"); +await new Promise((r) => setTimeout(r, 0)); +picker.extra.find((x) => x.kind === "combo").onPick(); await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0)); Object.assign(out, { sent, applied, payload: ctx.payload, toasts }); @@ -223,16 +227,12 @@ def test_화면_출처_다섯_사전조합_창_들여오기_줄_연결_TS(): ) # fmt: skip assert result.returncode == 0, result.stderr out = json.loads(result.stdout) - assert out["picks"] == [ - "일위대가 호표", - "단가산출 호표", - "구조물 조합", - "일위대가 사전조합", - "단가산출", - ] # 246-1 이름 셋(서고 · 층 · M01 글 없음) - assert out["more"] == ["연결 풀기"] + assert out["picks"] == ["단가연결"] # 251-6 고르기 다섯 = 단가연결 창 하나 + assert out["more"] == ["단가연결 풀기"] pre, (url, body) = out["sent"][0], out["sent"][1] - assert pre == ["pre", "personal", True, "일위대가 사전조합 고르기"] # 서고 줄만 · 층 셋 + kinds = ["logic", "precombo", "combo", "dg", "sg"] + assert pre == ["pre", "personal", kinds, "단가연결"] # 층 셋 단추 · 종류 고르기 + assert out["combo"] == [["구조물 조합", "관매설", "마스터 · SS000011"]] assert url == "/api/projects/P1/estimation/library" assert body == {"kind": "precombo", "layer": "company", "owner": "7", "key": "SC-000002"} assert out["sent"][2][1] == {"kind": "combo", "layer": "system", "owner": None, diff --git a/resources/tester/test_246_1_b09_bill_menu_simple.py b/resources/tester/test_246_1_b09_bill_menu_simple.py index 7a76c6545..450d2dd66 100644 --- a/resources/tester/test_246_1_b09_bill_menu_simple.py +++ b/resources/tester/test_246_1_b09_bill_menu_simple.py @@ -31,11 +31,12 @@ def test_더함_하나_줄_삭제_연결_이름_셋(): assert not set(OLD_ADDS) & set(out[key]) and "줄 지우기" not in out[key] assert "줄 삭제" in out["earth:4"] and "줄 삭제" in out["hand:a"] link = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Bill_Link.ts").read_text("utf-8") - picks = link.split("const picks: Leaf[] = [")[1].split("];")[0] + # 251-6 고르기 다섯 = 단가연결 모달 하나 · 종류 이름 = 단가산출 · 사전조합 · 구조물 조합 · 호표 둘 + kinds = link.split("kindFilter: [")[1].split("],\n pick")[0] for name in ["일위대가 호표", "단가산출 호표", "구조물 조합", "일위대가 사전조합", "단가산출"]: - assert f'["{name}",' in picks + assert f'text: "{name}"' in kinds for word in ["서고", "마스터", "회사", "개인", "M01", "새 산근", "로직"]: - assert word not in picks + assert word not in kinds toasts = [part.split(",")[0] for part in link.split("showToast(")[1:]] assert toasts and not any("서고" in t for t in toasts) # 알림 글 쉬운 말 bill = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Bill.ts").read_text("utf-8") diff --git a/resources/tester/test_246_2_context_submenu_side.py b/resources/tester/test_246_2_context_submenu_side.py index 3c09cc09a..f0bdc831c 100644 --- a/resources/tester/test_246_2_context_submenu_side.py +++ b/resources/tester/test_246_2_context_submenu_side.py @@ -165,4 +165,4 @@ def test_옛_아코디언_없음_공용_꼴(): assert ".ui-ctx-menu__sub" not in css and ".ui-ctx-menu__item.is-open" in css # B09 연결 = children 꼴 그대로(공용 메뉴가 옆 판으로) edit = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Bill_Edit.ts").read_text("utf-8") - assert 'parts[1].push({ label: "연결", children: picks }, ...more);' in edit + assert "parts[1].push(...picks, ...more);" in edit # 251-6 단가연결 = 모달 하나(하위 메뉴 없음)