diff --git a/B08_Quantity/B08_Quantity_UI_Drawing.css b/B08_Quantity/B08_Quantity_UI_Drawing.css index 02c7e9673..1b54d1c94 100644 --- a/B08_Quantity/B08_Quantity_UI_Drawing.css +++ b/B08_Quantity/B08_Quantity_UI_Drawing.css @@ -37,24 +37,25 @@ color: var(--color-danger); } -/* 카드 셋(도면 · 일위대가 · 면적산출) = 한 줄 세 칸 · 일위대가 · 면적산출 넓게(왼쪽) · 도면 맨 오른쪽(163-2) · +/* 카드 셋 = 한 줄 세 칸 · 차례 늘 도면 › 일위대가 › 면적산출 · 폭 2 : 3 : 3(170-1) · 높이 = 큰 창에 맞춤(156-4) */ .b08-sheets .m02-struct { --m02-struct-card-h: max(480px, calc(94vh - 14rem)); - grid-template-columns: minmax(0, 3fr) minmax(0, 3fr) minmax(0, 2fr); + grid-template-columns: minmax(0, 2fr) minmax(0, 3fr) minmax(0, 3fr); 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); } } +/* 머리 단추 칸(170-2) — 편집 화면 밖에선 숨김 */ +.b08-sheets .ui-modal__actions[hidden] { + display: none; +} + /* 카드 [수정] → 사본 편집 화면(163-3) — 모달 몸통을 채움 · 넣을 값이면 안내 한 줄 위 */ .b08-sheets .ui-item-picker__edit-host { flex-direction: column; @@ -88,6 +89,24 @@ border-color: var(--color-danger); } +/* 칩 테두리 상태 색(170-3) — 연결됨 · 마스터가 바뀜 · 고침 있음 · 원본 없어짐(빨강 점선) */ +.b08-link__chip.is-linked { + border-color: var(--color-success); +} + +.b08-link__chip.is-changed { + border-color: var(--color-warning); +} + +.b08-link__chip.is-edited { + border-color: var(--color-info); +} + +.b08-link__chip.is-gone { + border-style: dashed; + border-color: var(--color-danger); +} + .b08-link__state { overflow: hidden; max-width: 18rem; diff --git a/B08_Quantity/B08_Quantity_UI_Drawing.ts b/B08_Quantity/B08_Quantity_UI_Drawing.ts index 3270e135e..785ef79c4 100644 --- a/B08_Quantity/B08_Quantity_UI_Drawing.ts +++ b/B08_Quantity/B08_Quantity_UI_Drawing.ts @@ -145,6 +145,14 @@ export function openStructureSheets(request: SheetsRequest): void { let edited = false; // 사본을 고쳐 저장함 — 닫을 때 집계표 다시 셈(163-3) let editView: EditView | null = null; let modalBody: HTMLElement | null = null; + // 머리 단추 칸(170-2) — 공용 모달은 열 때 단추가 있어야 머리에 붙임 → 빈 칸을 미리 넘김 · 편집 화면에서만 채움 + const head = document.createElement("div"); + head.className = "ui-modal__actions"; + head.hidden = true; + const clearHead = (): void => { + head.replaceChildren(); + head.hidden = true; + }; /** 카드 [수정] — 몸통을 사본 편집 화면으로 · [뒤로] = 몸통 되돌리고 그 카드 다시 읽음 */ const openEdit: OpenEdit = (copy, name, kind, reload) => { const body = modalBody; @@ -170,12 +178,18 @@ export function openStructureSheets(request: SheetsRequest): void { (saved) => { view.destroy(); editView = null; + clearHead(); body.replaceChildren(...kept); edited ||= saved; if (saved) reload(); }, ); editView = view; + // [← 뒤로] · [저장] = 머리 [닫기] 왼쪽 · 크기 = [닫기](sm 뗌) + view.back.classList.remove("ui-btn--sm"); + view.save.classList.remove("ui-btn--sm"); + head.replaceChildren(view.back, view.save); + head.hidden = false; host.append( ...(kind === "inputs" ? [note(L("B08_Quantity_Sheets_InputsOnly"))] : []), view.root, @@ -192,9 +206,11 @@ export function openStructureSheets(request: SheetsRequest): void { closeLabel: L("Common_Btn_Close"), wide: true, // 공용 큰 창(96vw × 94vh · 156-4) dialogClass: "b08-sheets", + actions: [head], beforeClose: async () => { if (editView && !(await editView.leave())) return false; // 저장 안 한 고침 — 버릴지 물음 editView?.destroy(); + clearHead(); closed = true; // 아직 불러오는 중이면 뒤늦게 카드를 만들지 않음 handles.forEach((cards) => cards.destroy()); if (edited) request.onLinked(); // 사본을 고쳤으면 집계표 다시 셈(연결 저장과 같은 꼴) diff --git a/B08_Quantity/B08_Quantity_UI_Link.ts b/B08_Quantity/B08_Quantity_UI_Link.ts index e3865b214..0fa90c214 100644 --- a/B08_Quantity/B08_Quantity_UI_Link.ts +++ b/B08_Quantity/B08_Quantity_UI_Link.ts @@ -102,10 +102,11 @@ async function update(projectId: string, info: ProjectItemInfo): Promise { @@ -124,6 +125,15 @@ async function paintCopyState( 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)); + // 테두리 상태 색(170-3) — 앞섬 = 없어짐 › 바뀜 › 고침 › 연결 + const tone = info.원본없음 + ? "is-gone" + : info.마스터바뀜 + ? "is-changed" + : info.고침 + ? "is-edited" + : null; + if (tone) chip.classList.replace("is-linked", tone); button.disabled = !info.마스터바뀜 || !!info.원본없음; button.title = button.disabled ? L("B08_Quantity_Link_UpdateSame") @@ -163,7 +173,7 @@ export function linkSection( line.append(head); for (const slot of facility.slots) { const chip = document.createElement("span"); - chip.className = slot.linked ? "b08-link__chip" : "b08-link__chip is-missing"; + chip.className = slot.linked ? "b08-link__chip is-linked" : "b08-link__chip is-missing"; chip.dataset.b08LinkSlot = slot.copy; const name = document.createElement("span"); name.className = "b08-link__name"; @@ -204,7 +214,9 @@ export function linkSection( ); updateButton.disabled = true; // 원본판 대조를 받기 전엔 꺼 둠 buttons.append(updateButton); - void paintCopyState(projectId, slot, badges, updateButton).then((got) => (info = got)); + void paintCopyState(projectId, slot, chip, badges, updateButton).then( + (got) => (info = got), + ); } chip.append(name, state, badges, buttons); line.append(chip); diff --git a/B08_Quantity/B08_Quantity_UI_Page.css b/B08_Quantity/B08_Quantity_UI_Page.css index 46eb16e07..b419ee0de 100644 --- a/B08_Quantity/B08_Quantity_UI_Page.css +++ b/B08_Quantity/B08_Quantity_UI_Page.css @@ -19,6 +19,11 @@ border-bottom: 1px solid var(--color-border); } +/* 구조물 집계표 보조 탭 줄 자리(170-4) — 단추는 높이만 채움 */ +.b08-quantity__tabs--blank > .ui-btn { + visibility: hidden; +} + /* 자재 집계표 셋(121) — 안내 글 · [저장] 한 줄 */ .b08-material__toolbar { display: flex; diff --git a/B08_Quantity/B08_Quantity_UI_Page.ts b/B08_Quantity/B08_Quantity_UI_Page.ts index ab156c57c..78f27502f 100644 --- a/B08_Quantity/B08_Quantity_UI_Page.ts +++ b/B08_Quantity/B08_Quantity_UI_Page.ts @@ -87,7 +87,18 @@ function renderTab(host: HTMLElement, tab: TabId, projectId: string | null): voi const view = document.createElement("div"); host.replaceChildren(view); if (tab === "material") return renderMaterials(view, projectId); - if (tab === "summary" && projectId) return void renderStructureSummary(view, projectId); + if (tab === "summary" && projectId) { + // 보조 탭 줄 자리(170-4) — 보이지 않는 sm 단추 한 줄 · 자재 탭과 같은 높이라 탭 바꿀 때 안 튐 + const blank = document.createElement("div"); + blank.className = "b08-quantity__tabs b08-quantity__tabs--blank"; + blank.setAttribute("aria-hidden", "true"); + const ghost = createButton({ label: L(SHEETS[0].label), variant: "ghost", size: "sm" }); + ghost.tabIndex = -1; + blank.append(ghost); + const body = document.createElement("div"); + view.append(blank, body); + return void renderStructureSummary(body, projectId); + } if (tab === "hydrology" && projectId) return void renderHydro(view, projectId); if (tab === "earthwork" && projectId) return void renderEarthwork(view, projectId); view.append(pendingNote()); diff --git a/resources/tester/test_156_1_b08_unlink.py b/resources/tester/test_156_1_b08_unlink.py index 973fc7416..f37eaf7f7 100644 --- a/resources/tester/test_156_1_b08_unlink.py +++ b/resources/tester/test_156_1_b08_unlink.py @@ -109,7 +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 그대로 - # 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 + # 170-1 — 차례 늘 도면 › 일위대가 › 면적산출(뒤집기 없음) · 폭 2 : 3 : 3 + assert "minmax(0, 2fr) minmax(0, 3fr) minmax(0, 3fr)" in css + assert " order: " not in css assert ".b08-sheets__combo + .b08-sheets__combo" in css diff --git a/resources/tester/test_163_b08_modal.py b/resources/tester/test_163_b08_modal.py index b2a0d8166..732c7c7a5 100644 --- a/resources/tester/test_163_b08_modal.py +++ b/resources/tester/test_163_b08_modal.py @@ -17,7 +17,7 @@ LOCALE = (ROOT / "ui_template/ui_template_locale_b4.ts").read_text(encoding="utf 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 '"b08-link__chip is-linked"' 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 diff --git a/resources/tester/test_170_b08_modal_head.py b/resources/tester/test_170_b08_modal_head.py new file mode 100644 index 000000000..df73e7398 --- /dev/null +++ b/resources/tester/test_170_b08_modal_head.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +"""170 B08 구조물도 모달 — 머리 단추(170-2) · 칩 테두리 상태 색(170-3) · 집계표 보조 탭 줄 자리(170-4). + +화면 글 판정 — 공용 모달(ui_template_modal)은 안 고침. +""" + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +PAGE = (ROOT / "B08_Quantity/B08_Quantity_UI_Page.ts").read_text(encoding="utf-8") +PAGE_CSS = (ROOT / "B08_Quantity/B08_Quantity_UI_Page.css").read_text(encoding="utf-8") + + +def test_170_4_집계표_보조_탭_줄_자리(): + branch = PAGE.split('if (tab === "summary" && projectId) {', 1)[1].split("\n }", 1)[0] + # 자재 탭 보조 탭 줄과 같은 클래스 · sm 단추 하나(보이지 않음 · 초점 안 감) + assert '"b08-quantity__tabs b08-quantity__tabs--blank"' in branch + assert 'size: "sm"' in branch and "ghost.tabIndex = -1" in branch + assert 'setAttribute("aria-hidden", "true")' in branch + # 집계표는 줄 아래 몸통에 그림(replaceChildren 이 빈 줄을 지우지 않게) + assert ( + "view.append(blank, body)" in branch and "renderStructureSummary(body, projectId)" in branch + ) + assert ".b08-quantity__tabs--blank > .ui-btn {\n visibility: hidden;" in PAGE_CSS.replace( + "\r\n", "\n" + ) + + +LINK = (ROOT / "B08_Quantity/B08_Quantity_UI_Link.ts").read_text(encoding="utf-8") +DRAWING_CSS = (ROOT / "B08_Quantity/B08_Quantity_UI_Drawing.css").read_text(encoding="utf-8") +THEME = (ROOT / "ui_template/ui_template_theme.css").read_text(encoding="utf-8") + + +def test_170_3_칩_테두리_상태_색(): + assert 'slot.linked ? "b08-link__chip is-linked" : "b08-link__chip is-missing"' in LINK + assert "paintCopyState(projectId, slot, chip, badges, updateButton)" in LINK + # 앞섬 = 없어짐 › 바뀜 › 고침 › 연결 + tone = LINK.split("const tone = info.원본없음", 1)[1].split(";", 1)[0] + assert tone.index('"is-gone"') < tone.index('"is-changed"') < tone.index('"is-edited"') + assert 'chip.classList.replace("is-linked", tone)' in LINK + for cls, token in ( + ("is-linked", "success"), + ("is-missing", "danger"), + ("is-changed", "warning"), + ("is-edited", "info"), + ("is-gone", "danger"), + ): + rule = DRAWING_CSS.split(f".b08-link__chip.{cls} {{", 1)[1].split("}", 1)[0] + assert f"border-color: var(--color-{token});" in rule + assert f"--color-{token}:" in THEME # 밝음 · 어두움 토큰 + assert ( + "border-style: dashed;" + in DRAWING_CSS.split(".b08-link__chip.is-gone {", 1)[1].split("}", 1)[0] + ) + + +DRAWING = (ROOT / "B08_Quantity/B08_Quantity_UI_Drawing.ts").read_text(encoding="utf-8") +EDIT = (ROOT / "ui_template/ui_template_item_picker_edit.ts").read_text(encoding="utf-8") +PICKER = (ROOT / "ui_template/ui_template_item_picker.ts").read_text(encoding="utf-8") +MODAL = (ROOT / "ui_template/ui_template_modal.ts").read_text(encoding="utf-8") + + +def test_170_2_편집_화면_단추_모달_머리(): + # 편집 화면이 두 단추를 내보냄 · 고르기 모달(142-1)은 그대로 안 씀 + assert "back: HTMLButtonElement;" in EDIT and "save: HTMLButtonElement;" in EDIT + assert "return { root, back, save," in EDIT + assert "view.back" not in PICKER and "view.save" not in PICKER + # 빈 머리 칸을 열 때 미리 넘김(공용 모달은 mount 때 자식 있을 때만 머리에 붙임) + assert "if (actions.childElementCount) closeButton.before(actions)" in MODAL + assert "actions: [head]," in DRAWING and "head.hidden = true;" in DRAWING + # 들어갈 때 옮김 · 크기 = [닫기](md) · 차례 [← 뒤로] [저장] [닫기] + assert 'view.back.classList.remove("ui-btn--sm")' in DRAWING + assert 'view.save.classList.remove("ui-btn--sm")' in DRAWING + assert "head.replaceChildren(view.back, view.save);\n head.hidden = false;" in DRAWING + # 나올 때([뒤로] · 닫기) 거둠 + back = DRAWING.split("(saved) => {", 1)[1].split("},", 1)[0] + assert "clearHead();" in back + closing = DRAWING.split("beforeClose: async () => {", 1)[1].split("return true;", 1)[0] + assert "clearHead();" in closing + css = DRAWING_CSS + assert ".b08-sheets .ui-modal__actions[hidden] {\n display: none;" in css diff --git a/ui_template/ui_template_item_picker_edit.ts b/ui_template/ui_template_item_picker_edit.ts index 0958442c7..c39f4385b 100644 --- a/ui_template/ui_template_item_picker_edit.ts +++ b/ui_template/ui_template_item_picker_edit.ts @@ -20,6 +20,9 @@ import { laborValueData, PART_OF } from "./ui_template_item_picker_project"; export interface EditView { root: HTMLElement; + /** [← 뒤로] · [저장] — 모달 머리로 옮겨 쓸 수 있음(B08 · 170-2) */ + back: HTMLButtonElement; + save: HTMLButtonElement; /** 붙인 뒤 부름 — 편집 부품을 받아 세움 */ start: () => void; isDirty: () => boolean; @@ -212,5 +215,5 @@ export function createEditView(req: EditRequest, onBack: (saved: boolean) => voi if (await leave()) onBack(saved); }); - return { root, start: () => void start(), isDirty: () => dirty, leave, destroy }; + return { root, back, save, start: () => void start(), isDirty: () => dirty, leave, destroy }; }