feat(b08): 구조물도 모달 연결 칩 · 접기 · 카드 2:3:3 · 사본 [수정] · [업데이트] 배지(163-1~4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<boolean> {
|
||||
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<ProjectItemInfo | null> {
|
||||
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<boolean>): 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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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():
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user