diff --git a/B08_Quantity/B08_Quantity_UI_Drawing.css b/B08_Quantity/B08_Quantity_UI_Drawing.css index 23fa23e0c..02c7e9673 100644 --- a/B08_Quantity/B08_Quantity_UI_Drawing.css +++ b/B08_Quantity/B08_Quantity_UI_Drawing.css @@ -37,43 +37,89 @@ color: var(--color-danger); } -/* 카드 셋(도면 · 일위대가 · 면적산출) = 한 줄 세 칸 · 도면을 조금 넓게 · 높이 = 큰 창에 맞춤(156-4) */ +/* 카드 셋(도면 · 일위대가 · 면적산출) = 한 줄 세 칸 · 일위대가 · 면적산출 넓게(왼쪽) · 도면 맨 오른쪽(163-2) · + 높이 = 큰 창에 맞춤(156-4) */ .b08-sheets .m02-struct { --m02-struct-card-h: max(480px, calc(94vh - 14rem)); - grid-template-columns: minmax(0, 3fr) minmax(0, 2fr) minmax(0, 2fr); + grid-template-columns: minmax(0, 3fr) minmax(0, 3fr) minmax(0, 2fr); gap: var(--spacing-16); } +.b08-sheets .m02-struct > :first-child { + order: 3; +} + @media (max-width: 960px) { .b08-sheets .m02-struct { grid-template-columns: minmax(0, 1fr); } } -/* 「조합 연결」 칸(154-2) — 시설 이름 · 칸 줄(이름 · 연결 여부 · [선택]) */ +/* 카드 [수정] → 사본 편집 화면(163-3) — 모달 몸통을 채움 · 넣을 값이면 안내 한 줄 위 */ +.b08-sheets .ui-item-picker__edit-host { + flex-direction: column; + gap: var(--spacing-8); +} + +/* 「조합 연결」 칸(154-2 · 163-1) — 시설마다 한 줄(제목 + 칸 칩 · 넘치면 다음 줄) */ +.b08-link__row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--spacing-8); +} + .b08-link__facility { - margin: 0; font-weight: var(--font-weight-semibold); color: var(--color-text); } -.b08-link__slot { - display: grid; - grid-template-columns: minmax(120px, 200px) 1fr auto; +.b08-link__chip { + display: inline-flex; align-items: center; gap: var(--spacing-8); - padding-left: var(--spacing-16); + max-width: 100%; + padding: 2px var(--spacing-8); + border: 1px solid var(--color-border); + border-radius: var(--radius-pills); } -.b08-link__buttons { - display: flex; - gap: var(--spacing-8); +.b08-link__chip.is-missing { + border-color: var(--color-danger); } .b08-link__state { + overflow: hidden; + max-width: 18rem; + white-space: nowrap; + text-overflow: ellipsis; color: var(--color-text-secondary); } .b08-link__state.is-missing { color: var(--color-danger); } + +.b08-link__badges, +.b08-link__buttons { + display: inline-flex; + gap: var(--spacing-4); +} + +.b08-link__badges:empty { + display: none; +} + +/* 배지(163-4) — 고침 있음 · 마스터가 바뀜(경고) */ +.b08-link__badge { + padding: 0 var(--spacing-8); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + font-size: var(--text-caption); + color: var(--color-text); +} + +.b08-link__badge.is-warn { + border-color: var(--color-warning); + font-weight: var(--font-weight-semibold); +} diff --git a/B08_Quantity/B08_Quantity_UI_Drawing.ts b/B08_Quantity/B08_Quantity_UI_Drawing.ts index da5278966..3270e135e 100644 --- a/B08_Quantity/B08_Quantity_UI_Drawing.ts +++ b/B08_Quantity/B08_Quantity_UI_Drawing.ts @@ -4,7 +4,10 @@ * 조합 한 개 = M02 항목 카드(`M02_MasterTemplete_ItemCards`) 그대로 — 도면(도각 포함 · 보기 전용 CAD · * 여러 장이면 장 탭) · 일위대가 · 면적산출 = 공용 카드 셋을 한 줄 세 칸(156-4 · 창 = 공용 wide). * - * 원천 = 그 구조물의 프로젝트 사본만(연결된 사본 · PLAN 143-1) — 없으면 「사본 없음」 · 읽기만 + * 원천 = 그 구조물의 프로젝트 사본만(연결된 사본 · PLAN 143-1) — 없으면 「사본 없음」 + * 카드 [수정](163-3) = 모달 몸통이 사본 전용 편집 화면(고르기 모달과 같은 `createEditView`)으로 바뀜 · + * 도면 · 면적산출 = 그 부분 · 일위대가 카드 = 넣을 값만(담은로직 · 자재는 원본 층에서 · 서버 400 · 142-1) · + * [뒤로] = 카드 다시 읽음 · 저장했으면 닫을 때 집계표 다시 셈 · 판 다르면 서버 409 그대로 * 조합 목록 = 집계표와 같음(`B08_Quantity_Summary_Data` `combosAt`) * 시설이 있으면 맨 위 「조합 연결」 칸(`B08_Quantity_UI_Link` · PLAN 154-2 · 156-1) — 저장 · 끊기면 닫고 다시 셈 * 카드 · CAD · 스프레드시트는 모달을 열 때만 `import()` · 닫을 때 카드를 모두 정리 @@ -13,10 +16,12 @@ import "./B08_Quantity_UI_Drawing.css"; import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale"; import { openModal } from "@ui/ui_template_modal"; +import { showToast } from "@ui/ui_template_elements"; import { PART_OF, projectItems, withCopyValues } from "@ui/ui_template_item_picker_project"; +import type { EditView } from "@ui/ui_template_item_picker_edit"; import type { LaborDoc } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Api"; import type { ValueDoc } from "../M02_MasterTemplete/M02_Labor_Values_Model"; -import type { ItemCardsHandle } from "../M02_MasterTemplete/M02_MasterTemplete_ItemCards"; +import type { EditKind, ItemCardsHandle } from "../M02_MasterTemplete/M02_MasterTemplete_ItemCards"; import type { ItemSource } from "../M02_MasterTemplete/M02_MasterTemplete_ItemSource"; import { summaryTooltip, type SummaryItem } from "../common_util/common_util_summary_item"; import type { @@ -79,12 +84,21 @@ function sourceOf(projectId: string, folder: string): ItemSource { }; } +/** 카드 [수정] — 사본 · 고칠 부분(일위대가 카드 = 넣을 값) · 카드 다시 읽기 */ +type OpenEdit = ( + copy: LinkedSnapshot, + name: string, + kind: EditKind | "inputs", + reload: () => void, +) => void; + /** 조합 한 칸 — 제목줄(이름 · 원천) + 안내 + 카드. */ function comboSection( request: SheetsRequest, combo: StationCombo, createItemCards: typeof import("../M02_MasterTemplete/M02_MasterTemplete_ItemCards").createItemCards, handles: ItemCardsHandle[], + openEdit: OpenEdit, ): HTMLElement { const label = request.describe(combo.item); const name = label @@ -109,16 +123,18 @@ function comboSection( from.textContent = L("B08_Quantity_Sheets_FromCopy"); title.append(from); if (!copy.부분.includes("도면")) section.append(note(L("B08_Quantity_Drawing_NoDrawing"))); + const load = (): void => void cards.load({ id: copy.폴더, label: name }); + // 카드 안 [저장]은 없음(자료 자리에 write 없음) — [수정]만 사본 편집 화면으로(163-3) const cards = createItemCards({ source: () => source, - editable: () => false, - onEdit: () => undefined, + editable: () => true, + onEdit: (kind) => openEdit(copy, name, kind, load), warnOnFail: () => false, // 못 읽으면 카드에 「없음」 - labor: () => undefined, // 일위대가 카드(161-1) — 보기 전용이라 [수정] 숨김(editable false) + labor: () => openEdit(copy, name, "inputs", load), // 일위대가 카드(161-1) [수정] = 넣을 값만 }); handles.push(cards); section.append(cards.root); - void cards.load({ id: copy.폴더, label: name }); + load(); return section; } @@ -126,17 +142,66 @@ function comboSection( export function openStructureSheets(request: SheetsRequest): void { const handles: ItemCardsHandle[] = []; let closed = false; + let edited = false; // 사본을 고쳐 저장함 — 닫을 때 집계표 다시 셈(163-3) + let editView: EditView | null = null; + let modalBody: HTMLElement | null = null; + /** 카드 [수정] — 몸통을 사본 편집 화면으로 · [뒤로] = 몸통 되돌리고 그 카드 다시 읽음 */ + const openEdit: OpenEdit = (copy, name, kind, reload) => { + const body = modalBody; + if (!body || editView) return; + void (async () => { + try { + const [{ createEditView }] = await Promise.all([ + import("@ui/ui_template_item_picker_edit"), + import("@ui/ui_template_item_picker.css"), + ]); + if (closed) return; + const kept = [...body.children]; + const host = document.createElement("div"); + host.className = "ui-item-picker__edit-host"; + const view = createEditView( + { + kind, + row: { key: copy.항목.key, 열: copy.항목.key, 경로: [name] }, + info: { 폴더: copy.폴더, 원본: copy.항목 }, + items: projectItems(request.projectId), + projectId: request.projectId, + }, + (saved) => { + view.destroy(); + editView = null; + body.replaceChildren(...kept); + edited ||= saved; + if (saved) reload(); + }, + ); + editView = view; + host.append( + ...(kind === "inputs" ? [note(L("B08_Quantity_Sheets_InputsOnly"))] : []), + view.root, + ); + body.replaceChildren(host); + view.start(); + } catch (error) { + showToast(`${L("B08_Quantity_Drawing_Failed")} — ${String(error)}`, "error"); + } + })(); + }; openModal({ title: `${request.station} ${L("B08_Quantity_Sheets_Title")}`, closeLabel: L("Common_Btn_Close"), wide: true, // 공용 큰 창(96vw × 94vh · 156-4) dialogClass: "b08-sheets", - beforeClose: () => { + beforeClose: async () => { + if (editView && !(await editView.leave())) return false; // 저장 안 한 고침 — 버릴지 물음 + editView?.destroy(); closed = true; // 아직 불러오는 중이면 뒤늦게 카드를 만들지 않음 handles.forEach((cards) => cards.destroy()); + if (edited) request.onLinked(); // 사본을 고쳤으면 집계표 다시 셈(연결 저장과 같은 꼴) return true; }, mount: (body, close) => { + modalBody = body; const linking = request.facilities.length ? [ linkSection(request.projectId, request.facilities, () => { @@ -161,7 +226,7 @@ export function openStructureSheets(request: SheetsRequest): void { body.replaceChildren( ...linking, ...request.combos.map((combo) => - comboSection(request, combo, createItemCards, handles), + comboSection(request, combo, createItemCards, handles, openEdit), ), ); } catch (error) { diff --git a/B08_Quantity/B08_Quantity_UI_Link.ts b/B08_Quantity/B08_Quantity_UI_Link.ts index 2f8329d3e..e3865b214 100644 --- a/B08_Quantity/B08_Quantity_UI_Link.ts +++ b/B08_Quantity/B08_Quantity_UI_Link.ts @@ -1,17 +1,25 @@ /* ============================================================================= * B08_Quantity_UI_Link.ts - * 구조물도 모달 맨 위 「조합 연결」 칸(PLAN 154-2 · 156) — 그 측점 시설의 칸 전부 + 연결 여부. + * 구조물도 모달 맨 위 「조합 연결」 칸(PLAN 154-2 · 156 · 163) — 그 측점 시설의 칸 전부 + 연결 여부. * + * 꼴(163-1) = 시설마다 한 줄(제목 + 칸 칩 · flex-wrap) · 공용 접기(모두 연결이면 접힘 · 미연결 있으면 펼침) * [선택] → 공용 고르기 모달(`openItemPicker` · 누를 때 `import()`) → 바로 저장 * = PUT …/items/{사본 id}/link(그 칸 하나만 정본에 · 서버가 사본 맞춤) → 집계표 다시 셈 * [연결 끊기](연결된 칸만) → 확인 창 → 같은 길에 null(그 칸만 비움 · 사본 폴더 그대로 · 156-1) - * 단추 = 모달 머리 [닫기]와 같은 양식(ghost · 기본 크기 · 156-3) + * [업데이트](연결된 칸만 · 163-4) = 칩 그린 뒤 `list(사본 폴더)` 로 배지(마스터가 바뀜 · 고침 있음 · + * 원본이 없어짐) · 마스터가 바뀜일 때만 켬 → 확인 창 → 고르기 모달 [재적용]과 같은 길(`reapply`) + * 단추 = 작은 ghost(칩 안 · 163-1) * B05/B06 세션 초안(구조물 · 관)이 있으면 막고 알림 — 그쪽 [저장]이 목록 통째로 덮어 연결이 사라짐 * ========================================================================== */ import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale"; import { createButton, showConfirmDialog, showToast } from "@ui/ui_template_elements"; -import { projectItems } from "@ui/ui_template_item_picker_project"; +import { attachCollapsible } from "@ui/ui_template_collapsible"; +import { + projectItems, + structureFolder, + type ProjectItemInfo, +} from "@ui/ui_template_item_picker_project"; import { readPendingStructures } from "../B05_Profile/B05_Profile_Api_Structures"; import { readPendingPipes } from "../B05_Profile/B05_Profile_Api_Pipes_Draft"; import type { SummaryItem } from "../common_util/common_util_summary_item"; @@ -74,57 +82,134 @@ async function unlink(projectId: string, facility: UnlinkedFacility, slot: Unlin } } -/** 「조합 연결」 칸 — 시설마다 이름 + 칸 줄(이름 · 연결 여부 · [선택]). `onLinked` = 저장 뒤(모달 닫고 다시 셈). */ +/** [업데이트] — 사본을 원본으로 덮음(입력값 · 고른자재는 같은 키면 서버가 살림) · 했으면 true. */ +async function update(projectId: string, info: ProjectItemInfo): Promise { + const ask = + L("B08_Quantity_Link_UpdateAsk") + (info.고침 ? ` ${L("B08_Quantity_Link_UpdateEdited")}` : ""); + if (!(await showConfirmDialog(ask, L("B08_Quantity_Link_Update")))) return false; + try { + const got = await projectItems(projectId).reapply(info); + showToast( + got.covered.length + ? `${L("B08_Quantity_Link_UpdateDone")} — ${got.covered.join(" · ")}` + : L("B08_Quantity_Link_UpdateNone"), + "success", + ); + return true; + } catch (error) { + showToast(`${L("B08_Quantity_Link_UpdateFailed")} — ${String(error)}`, "error"); + return false; + } +} + +/** 칩 배지 · [업데이트] — 사본 목록(원본판 대조)을 뒤에서 받아 칠함 · 못 받으면 단추 꺼진 채. */ +async function paintCopyState( + projectId: string, + slot: UnlinkedSlot, + badges: HTMLElement, + button: HTMLButtonElement, +): Promise { + const folder = structureFolder(slot.copy); + const info = await projectItems(projectId) + .list(folder) + .then((rows) => rows.find((row) => row.폴더 === folder) ?? null) + .catch(() => null); + if (!info) return null; + const badge = (key: keyof typeof ui_locales, warn: boolean): HTMLElement => { + const node = document.createElement("span"); + node.className = warn ? "b08-link__badge is-warn" : "b08-link__badge"; + node.textContent = L(key); + return node; + }; + if (info.고침) badges.append(badge("B08_Quantity_Link_Edited", false)); + if (info.원본없음) badges.append(badge("B08_Quantity_Link_Gone", true)); + else if (info.마스터바뀜) badges.append(badge("B08_Quantity_Link_Changed", true)); + button.disabled = !info.마스터바뀜 || !!info.원본없음; + button.title = button.disabled + ? L("B08_Quantity_Link_UpdateSame") + : L("B08_Quantity_Link_UpdateTip"); + return info; +} + +/** 「조합 연결」 칸 — 시설마다 한 줄(제목 + 칸 칩) · 공용 접기. `onLinked` = 저장 뒤(모달 닫고 다시 셈). */ export function linkSection( projectId: string, facilities: readonly UnlinkedFacility[], onLinked: () => void, ): HTMLElement { + const slots = facilities.flatMap((f) => f.slots); + const missing = slots.filter((slot) => !slot.linked).length; const section = document.createElement("section"); - section.className = "b08-sheets__combo b08-link"; + // 공용 접기(163-1) — 모두 연결이면 접힘 · 미연결 있으면 펼침 + section.className = `b08-sheets__combo b08-link ui-collapsible${missing ? "" : " is-collapsed"}`; section.dataset.b08Link = "section"; const title = document.createElement("h4"); - title.className = "b08-sheets__title"; - title.textContent = L("B08_Quantity_Link_Title"); + title.className = "b08-sheets__title ui-collapsible__title"; + const count = document.createElement("span"); + count.className = "b08-sheets__from"; + count.textContent = `${L("B08_Quantity_Link_Linked")} ${slots.length - missing} / ${slots.length}`; + const label = document.createElement("span"); + label.textContent = L("B08_Quantity_Link_Title"); + label.append(" ", count); + title.append(label); section.append(title); + attachCollapsible(section); for (const facility of facilities) { - const head = document.createElement("p"); + const line = document.createElement("div"); + line.className = "b08-link__row"; + const head = document.createElement("span"); head.className = "b08-link__facility"; head.textContent = facility.name; - section.append(head); + line.append(head); for (const slot of facility.slots) { - const row = document.createElement("div"); - row.className = "b08-link__slot"; - row.dataset.b08LinkSlot = slot.copy; + const chip = document.createElement("span"); + chip.className = slot.linked ? "b08-link__chip" : "b08-link__chip is-missing"; + chip.dataset.b08LinkSlot = slot.copy; const name = document.createElement("span"); + name.className = "b08-link__name"; name.textContent = slot.group ?? L("B08_Quantity_Link_Body"); const state = document.createElement("span"); state.className = slot.linked ? "b08-link__state" : "b08-link__state is-missing"; state.textContent = slot.linked - ? `${L("B08_Quantity_Link_Linked")} · ${slot.item?.name ?? ""}` + ? (slot.item?.name ?? L("B08_Quantity_Link_Linked")) : L("B08_Quantity_Summary_Unlinked"); + state.title = state.textContent; // 칩이 좁으면 긴 조합 이름이 잘림 + const badges = document.createElement("span"); + badges.className = "b08-link__badges"; const buttons = document.createElement("span"); buttons.className = "b08-link__buttons"; - const act = (label: string, run: typeof pickAndLink): HTMLButtonElement => { - const button = createButton({ + const act = (label: string, run: () => Promise): HTMLButtonElement => + createButton({ label, - variant: "ghost", // 모달 [닫기]와 같은 양식(156-3) + variant: "ghost", + size: "sm", // 칩 안 작은 단추(163-1) onClick: () => { + const was = [...buttons.querySelectorAll("button")].map((b) => b.disabled); buttons.querySelectorAll("button").forEach((b) => (b.disabled = true)); - void run(projectId, facility, slot).then((saved) => + void run().then((saved) => saved ? onLinked() - : buttons.querySelectorAll("button").forEach((b) => (b.disabled = false)), + : buttons.querySelectorAll("button").forEach((b, i) => (b.disabled = was[i])), ); }, }); - return button; - }; - buttons.append(act(L("B08_Quantity_Link_Pick"), pickAndLink)); - if (slot.linked) buttons.append(act(L("B08_Quantity_Link_Unlink"), unlink)); - row.append(name, state, buttons); - section.append(row); + buttons.append( + act(L("B08_Quantity_Link_Pick"), () => pickAndLink(projectId, facility, slot)), + ); + if (slot.linked) { + buttons.append(act(L("B08_Quantity_Link_Unlink"), () => unlink(projectId, facility, slot))); + let info: ProjectItemInfo | null = null; + const updateButton = act(L("B08_Quantity_Link_Update"), () => + info ? update(projectId, info) : Promise.resolve(false), + ); + updateButton.disabled = true; // 원본판 대조를 받기 전엔 꺼 둠 + buttons.append(updateButton); + void paintCopyState(projectId, slot, badges, updateButton).then((got) => (info = got)); + } + chip.append(name, state, badges, buttons); + line.append(chip); } + section.append(line); } return section; } diff --git a/resources/tester/test_156_1_b08_unlink.py b/resources/tester/test_156_1_b08_unlink.py index 501aa61b3..973fc7416 100644 --- a/resources/tester/test_156_1_b08_unlink.py +++ b/resources/tester/test_156_1_b08_unlink.py @@ -90,11 +90,12 @@ def test_집계_모든_시설_칸_끊으면_미연결(root, monkeypatch): def test_화면_연결된_칸만_끊기_닫기_양식(): ui = (ROOT / "B08_Quantity/B08_Quantity_UI_Link.ts").read_text(encoding="utf-8") - assert 'if (slot.linked) buttons.append(act(L("B08_Quantity_Link_Unlink"), unlink))' in ui + assert "if (slot.linked) {" in ui + assert 'act(L("B08_Quantity_Link_Unlink"), () => unlink(projectId, facility, slot))' in ui assert ".link(slot.copy, facility.facility, null)" in ui assert 'showConfirmDialog(L("B08_Quantity_Link_UnlinkAsk")' in ui assert ui.count("hasPendingDesign(projectId)") == 2 # 선택 · 끊기 둘 다 막음 - assert 'size: "sm"' not in ui # 모달 [닫기] = ghost · 기본 크기 + assert 'variant: "ghost",\n size: "sm"' in ui # 칩 안 작은 ghost(163-1) modal = (ROOT / "ui_template/ui_template_modal.ts").read_text(encoding="utf-8") assert 'label: opts.closeLabel,\n variant: "ghost",\n onClick' in modal page = (ROOT / "B08_Quantity/B08_Quantity_UI_Summary.ts").read_text(encoding="utf-8") @@ -108,5 +109,7 @@ def test_156_4_구조물도_모달_큰_창_세_칸(): assert "wide: true" in sheets css = (ROOT / "B08_Quantity/B08_Quantity_UI_Drawing.css").read_text(encoding="utf-8") assert "min(1320px" not in css # 자기 폭 없음 — 공용 wide 그대로 - assert "minmax(0, 3fr) minmax(0, 2fr) minmax(0, 2fr)" in css # 도면 · 일위대가 · 면적산출 + # 163-2 — 일위대가 · 면적산출 넓게(왼쪽) · 도면 좁게 맨 오른쪽(order 3) + assert "minmax(0, 3fr) minmax(0, 3fr) minmax(0, 2fr)" in css + assert ".b08-sheets .m02-struct > :first-child {\n order: 3;" in css assert ".b08-sheets__combo + .b08-sheets__combo" in css diff --git a/resources/tester/test_161_1_b08_labor_card.py b/resources/tester/test_161_1_b08_labor_card.py index fd21d4c5e..d1708e07c 100644 --- a/resources/tester/test_161_1_b08_labor_card.py +++ b/resources/tester/test_161_1_b08_labor_card.py @@ -11,8 +11,8 @@ def test_labor_handle_passed(): # createItemCards 에 labor 손잡이 — 없으면 카드 자체를 안 만듦(ItemCards `opts.labor ?`) call = DRAWING[DRAWING.index("createItemCards({") :] call = call[: call.index("});")] - assert "labor: () => undefined" in call - assert "editable: () => false" in call # [수정] 숨김 + assert 'labor: () => openEdit(copy, name, "inputs", load)' in call # 163-3 넣을 값만 + assert "editable: () => true" in call assert "const laborCard = opts.labor" in CARDS diff --git a/resources/tester/test_163_b08_modal.py b/resources/tester/test_163_b08_modal.py new file mode 100644 index 000000000..b2a0d8166 --- /dev/null +++ b/resources/tester/test_163_b08_modal.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +"""163 B08 구조물도 모달 — 연결 칩 · 접기(163-1) · 사본 고침(163-3) · [업데이트](163-4) · 콘크리트소요 줄(163-5). + +화면 글 판정 — 서버 · 짝 계산(Material_Calc)은 안 고침 · 고르기 모달 편집 화면은 재사용만. +""" + +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +LINK = (ROOT / "B08_Quantity/B08_Quantity_UI_Link.ts").read_text(encoding="utf-8") +DRAWING = (ROOT / "B08_Quantity/B08_Quantity_UI_Drawing.ts").read_text(encoding="utf-8") +CSS = (ROOT / "B08_Quantity/B08_Quantity_UI_Drawing.css").read_text(encoding="utf-8") +MATERIAL = (ROOT / "B08_Quantity/B08_Quantity_UI_Material.ts").read_text(encoding="utf-8") +LOCALE = (ROOT / "ui_template/ui_template_locale_b4.ts").read_text(encoding="utf-8") + + +def test_163_1_칩_한_줄_공용_접기(): + assert 'line.className = "b08-link__row"' in LINK # 시설마다 한 줄 + assert '"b08-link__chip"' in LINK and "chip.dataset.b08LinkSlot = slot.copy" in LINK + # 모두 연결이면 접힘 · 미연결 있으면 펼침 + assert 'ui-collapsible${missing ? "" : " is-collapsed"}' in LINK + assert "ui-collapsible__title" in LINK and "attachCollapsible(section)" in LINK + assert "state.title = state.textContent" in LINK # 긴 이름 = 툴팁 + assert re.search(r"\.b08-link__row \{[^}]*flex-wrap: wrap", CSS) + assert ".b08-link__slot" not in CSS + LINK # 옛 줄 꼴 없음 + + +def test_163_3_수정_사본_편집_화면(): + assert "editable: () => true" in DRAWING + assert "onEdit: (kind) => openEdit(copy, name, kind, load)" in DRAWING + assert 'labor: () => openEdit(copy, name, "inputs", load)' in DRAWING + assert 'import("@ui/ui_template_item_picker_edit")' in DRAWING + assert "createEditView(" in DRAWING and "items: projectItems(request.projectId)" in DRAWING + assert "info: { 폴더: copy.폴더, 원본: copy.항목 }" in DRAWING + # [뒤로] = 몸통 되돌림 · 저장했으면 카드 다시 읽음 · 닫을 때 집계표 다시 셈 + assert "body.replaceChildren(...kept)" in DRAWING and "if (saved) reload()" in DRAWING + assert "if (edited) request.onLinked()" in DRAWING + assert "if (editView && !(await editView.leave())) return false" in DRAWING + assert 'kind === "inputs" ? [note(L("B08_Quantity_Sheets_InputsOnly"))]' in DRAWING + assert "saveTemplate" not in DRAWING # 층 쓰기 없음 — 사본 PUT 만(편집 화면 `items.write`) + + +def test_163_4_배지_업데이트(): + assert ".list(folder)" in LINK and "structureFolder(slot.copy)" in LINK + for key in ("Edited", "Gone", "Changed"): + assert f'badge("B08_Quantity_Link_{key}"' in LINK, key + assert "button.disabled = !info.마스터바뀜 || !!info.원본없음" in LINK + assert "updateButton.disabled = true" in LINK # 대조 받기 전 꺼짐 + assert "projectItems(projectId).reapply(info)" in LINK + assert 'showConfirmDialog(ask, L("B08_Quantity_Link_Update"))' in LINK + assert 'info.고침 ? ` ${L("B08_Quantity_Link_UpdateEdited")}`' in LINK + # 저장 뒤 = [선택]과 같은 흐름(onLinked) + assert "saved\n ? onLinked()" in LINK + + +def test_163_5_콘크리트_5줄_늘_까닭(): + table = MATERIAL[ + MATERIAL.index("function concreteTable") : MATERIAL.index("function totalTable") + ] + assert "const keep = rows.map((_row, i) => i)" in table + assert 'editable: (_i, key) => key.startsWith("per:")' in table # 배합 늘 고침 + assert "function concreteWhy(" in MATERIAL + assert "matched(m.name, m.spec, m.unit)" in MATERIAL # 짝 계산 맞춤 그대로 + assert "source.snapshot?.units?.[row.id]" in MATERIAL + assert "concreteNote.textContent = concreteWhy(sheets, payload.source)" in MATERIAL + assert "Material_Calc" in MATERIAL and "concreteWhy" not in ( + ROOT / "B08_Quantity/B08_Quantity_Material_Calc.ts" + ).read_text(encoding="utf-8") + + +def test_locale_keys_exist(): + for text in (LINK, DRAWING): + for key in set(re.findall(r'L\("([A-Za-z0-9_]+)"\)', text)) | set( + re.findall(r'badge\("([A-Za-z0-9_]+)"', text) + ): + assert f"{key}:" in LOCALE or key.startswith(("B08_Quantity_Summary_", "Common_")), key diff --git a/resources/tester/test_61_2_b08_structure_sheets.py b/resources/tester/test_61_2_b08_structure_sheets.py index 694ae2737..d390057d8 100644 --- a/resources/tester/test_61_2_b08_structure_sheets.py +++ b/resources/tester/test_61_2_b08_structure_sheets.py @@ -25,11 +25,11 @@ def test_modal_own_class_and_cleanup(): assert 'dialogClass: "b08-sheets"' in DRAWING -def test_read_only(): - # 쓰는 길이 없음 — 저장 · 고침 · PUT/POST 없음 · 카드는 고칠 수 없음 +def test_no_layer_write(): + # 층 쓰기 · 카드 안 저장 없음 — 고침은 [수정] → 사본 편집 화면만(163-3 · 사본 PUT) for word in ("saveTemplate", "write:", "method:", "sheetEdit", "onImport", "onClear"): assert word not in DRAWING, word - assert "editable: () => false" in DRAWING + assert "editable: () => true" in DRAWING def test_copy_only(): diff --git a/ui_template/ui_template_locale_b4.ts b/ui_template/ui_template_locale_b4.ts index 970bc5ae4..bc300a4ee 100644 --- a/ui_template/ui_template_locale_b4.ts +++ b/ui_template/ui_template_locale_b4.ts @@ -29,6 +29,10 @@ export const ui_locales_b4 = { B08_Quantity_Sheets_Title: ["구조물도", "Structure Sheets"], B08_Quantity_Sheets_FromCopy: ["프로젝트 복사본", "Project copy"], B08_Quantity_Sheets_NoCopy: ["복사본에 없음", "Not in the project copy"], + B08_Quantity_Sheets_InputsOnly: [ + "일위대가는 넣을 값만 이 사본에서 고침 — 담은로직 · 자재는 원본 층(M02)에서 고침", + "Only inputs are edited in this copy — logic lines and materials are edited in the source layer (M02)", + ], B08_Quantity_Drawing_Loading: ["구조물도를 불러오는 중…", "Loading structure sheets…"], B08_Quantity_Drawing_Failed: [ "구조물도를 불러오지 못했습니다", @@ -77,6 +81,32 @@ export const ui_locales_b4 = { ], B08_Quantity_Link_Unlinked: ["조합 연결을 끊었습니다.", "Combination unlinked."], B08_Quantity_Link_UnlinkFailed: ["연결을 끊지 못했습니다", "Failed to unlink"], + B08_Quantity_Link_Update: ["업데이트", "Update"], + B08_Quantity_Link_UpdateTip: [ + "마스터가 바뀜 — 원본을 다시 가져와 이 사본에 덮음", + "The master changed — re-import the source into this copy", + ], + B08_Quantity_Link_UpdateSame: [ + "원본과 같음 — 업데이트할 것 없음", + "Same as the source — nothing to update", + ], + B08_Quantity_Link_UpdateAsk: [ + "원본을 다시 가져와 이 사본에 덮습니다 — 입력값 · 고른 자재는 살리고(같은 키) 도면 · 면적산출 · 일위대가는 원본 것으로 덮습니다.", + "Re-import the source into this copy — inputs and picked materials are kept (same keys); drawing, area sheet and unit-price sheet are overwritten.", + ], + B08_Quantity_Link_UpdateEdited: [ + "이 사본에서 고친 것이 덮입니다.", + "Edits made in this copy will be overwritten.", + ], + B08_Quantity_Link_UpdateDone: ["업데이트했습니다 — 덮인 것", "Updated — overwritten"], + B08_Quantity_Link_UpdateNone: [ + "업데이트했습니다 — 덮인 것 없음(원본과 같았음)", + "Updated — nothing overwritten (same as the source)", + ], + B08_Quantity_Link_UpdateFailed: ["업데이트하지 못했습니다", "Failed to update"], + B08_Quantity_Link_Changed: ["마스터가 바뀜", "Master changed"], + B08_Quantity_Link_Edited: ["고침 있음", "Edited"], + B08_Quantity_Link_Gone: ["원본이 없어짐", "Source gone"], B08_Quantity_Link_Pending: [ "B05 · B06 에 저장 안 한 조작이 있습니다 — B05 · B06 [저장] 먼저 누른 뒤 연결하세요.", "Unsaved edits in B05 · B06 — save there first, then link.",