Merge remote-tracking branch 'origin/sub_laptop_8' into main_laptop_1

This commit is contained in:
2026-10-10 08:48:49 +09:00
9 changed files with 162 additions and 15 deletions
+25 -6
View File
@@ -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;
+16
View File
@@ -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(); // 사본을 고쳤으면 집계표 다시 셈(연결 저장과 같은 꼴)
+15 -3
View File
@@ -102,10 +102,11 @@ async function update(projectId: string, info: ProjectItemInfo): Promise<boolean
}
}
/** 칩 배지 · [업데이트] — 사본 목록(원본판 대조)을 뒤에서 받아 칠함 · 못 받으면 단추 꺼진 채. */
/** 칩 배지 · 테두리 색 · [업데이트] — 사본 목록(원본판 대조)을 뒤에서 받아 칠함 · 못 받으면 단추 꺼진 채. */
async function paintCopyState(
projectId: string,
slot: UnlinkedSlot,
chip: HTMLElement,
badges: HTMLElement,
button: HTMLButtonElement,
): Promise<ProjectItemInfo | null> {
@@ -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);
+5
View File
@@ -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;
+12 -1
View File
@@ -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());
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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
@@ -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
+4 -1
View File
@@ -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 };
}