feat(M02): 항목 추가 창 = 종류 + 이름 + 일위대가 · 카드 넷 2×2 · 표 접기 (PLAN 31-2)
- 항목 추가(나무 「항목 추가」 · 「새 항목」) — 구조물 종류(있는 것 고름 · 새 이름 · 폴더를 골라 두었으면 그 안) + 이름 + 일위대가(세 층 조합 고르기 창 · 찾기 AND · 비워도 됨) · 단위 칸 없앰 · 빈 칸 안내 · 서버 add-item 이 일위대가 받음 - 일위대가 고르기 창을 pickCombo 로 내보내 표 칸 고르기와 같이 씀 - 카드 넷(도면 정보 · 면적산출 · 재료산출 · 선택 입력) 2행 2열 · 높이 360px · 넘치면 카드 안 스크롤 · 상세 칸 전체 스크롤(좁으면 1열) - 「구조물 조합」 제목을 누르면 표 접힘 · 펴짐 - 시험 — 항목 추가 서버 · 창 · 격자 · 접기 · 옛 단위 필수 시험을 31 정함으로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -447,6 +447,8 @@ export interface TreeEdit {
|
||||
새경로?: string[];
|
||||
키?: string;
|
||||
단위?: string;
|
||||
/** 항목 추가 때 고른 일위대가 조합 키(PLAN 31-2) */
|
||||
일위대가?: string;
|
||||
}
|
||||
|
||||
/** `owner` = 남의 층(없으면 자기 층) — 지금 층과 따로 받음(나무 부품이 여러 뿌리를 그림) */
|
||||
|
||||
@@ -53,6 +53,7 @@ class TreeBody(BaseModel):
|
||||
새경로: list[str] | None = None
|
||||
키: str | None = None
|
||||
단위: str | None = None
|
||||
일위대가: str | None = None # 항목 추가 때 고른 조합 키(PLAN 31-2)
|
||||
판: str
|
||||
# 여러 개 옮기기 · 지우기(PLAN 30-2) — `[{경로, 키?}]` · 있으면 이것마다 차례로 · 저장 한 번
|
||||
대상: list[dict[str, Any]] | None = None
|
||||
|
||||
@@ -6,39 +6,55 @@
|
||||
display: contents;
|
||||
}
|
||||
|
||||
/* 카드 셋 나란히(같은 폭) · 카드 하나가 300px 안 되면 자동으로 2열 → 1열(칸 폭에 따라 · 창이 작아지면 바로 바뀜) */
|
||||
/* 카드 넷(도면 정보 · 면적산출 · 재료산출 · 선택 입력) = 2행 2열 · 카드 높이 고정(PLAN 31-2) ·
|
||||
칸이 좁으면(720px 아래) 1열 */
|
||||
.m02-struct {
|
||||
/* CAD 미리보기 · 면적산출 · 재료산출 내용 최소 높이 — 이보다 작아질 때만 화면이 스크롤 */
|
||||
--m02-struct-body-min: 16rem;
|
||||
/* 카드 본문은 카드 높이를 채움 — 넘치면 카드 안에서 스크롤 */
|
||||
--m02-struct-body-min: 0;
|
||||
--m02-struct-card-h: 360px;
|
||||
display: grid;
|
||||
flex: 1 1 0; /* 메인 칸(창 높이)에서 표 아래 남은 높이를 다 차지 */
|
||||
grid-auto-rows: minmax(auto, 1fr); /* 카드 셋 같은 높이 */
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
|
||||
flex: none;
|
||||
grid-auto-rows: var(--m02-struct-card-h);
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
padding-top: var(--spacing-12);
|
||||
border-top: 1px solid var(--color-border);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.m02-struct {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.m02-struct > * {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 집계표 칸을 골라 카드가 보이면 메인 칸을 창 높이로 고정 — 표 아래 남은 높이만큼만 카드(스크롤 없음) */
|
||||
/* 카드가 보이면 상세 칸은 창 높이까지 — 표 + 카드가 넘치면 상세 칸 전체가 스크롤 */
|
||||
.m02-main:has(> .m02-main__extra > .m02-struct:not([hidden])) {
|
||||
box-sizing: border-box;
|
||||
height: calc(100vh - var(--spacing-64));
|
||||
max-height: calc(100vh - var(--spacing-64));
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 표는 줄어들지 않음(줄면 표가 카드 위로 넘침) — 창이 작으면 카드가 최소 높이에서 멈추고 화면이 스크롤 */
|
||||
/* 표는 줄어들지 않음(줄면 표가 카드 위로 넘침) */
|
||||
.m02-main:has(> .m02-main__extra > .m02-struct:not([hidden])) > .m02-main__host {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 카드 = 제목 + 머리 + 본문 — 본문이 남는 높이를 채움 */
|
||||
/* 카드 = 제목 + 머리 + 본문 — 본문이 남는 높이를 채움 · 넘치면 카드 안 스크롤 */
|
||||
.m02-struct__card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.m02-struct__card > :last-child {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* ui-sidebar-panel 은 display:flex 라 hidden 속성을 덮어씀 — 안 보일 땐 확실히 안 보이게 */
|
||||
|
||||
@@ -166,6 +166,34 @@ function choose(title: string, items: Choice[], clearable: boolean): Promise<str
|
||||
});
|
||||
}
|
||||
|
||||
/** 일위대가 고르기 창 — 이 층이 쓸 수 있는 세 층 조합 · 찾기 = 띄어쓰기 AND · 「비움」 = 키 빈 글 · 닫으면 null
|
||||
* (표 칸 고르기 · 항목 추가 창이 함께 씀 · PLAN 31-2) */
|
||||
export async function pickCombo(title: string): Promise<{ key: string; label: string } | null> {
|
||||
let list: Awaited<ReturnType<typeof loadCombos>>;
|
||||
try {
|
||||
list = await loadCombos();
|
||||
} catch (error) {
|
||||
showToast(fill(TEXT.combosFailed, why(error)), "error");
|
||||
return null;
|
||||
}
|
||||
const items = list.map((c) => ({
|
||||
value: c.키,
|
||||
key: c.키,
|
||||
// 이름에 단위가 이미 붙은 조합이 있음 — 겹치면 뺌
|
||||
label: [
|
||||
c.이름,
|
||||
c.단위 && !c.이름.includes(`(${c.단위})`) && `(${c.단위})`,
|
||||
c.구분,
|
||||
`· ${LAYER_WORD[c.layer]}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
}));
|
||||
const key = await choose(title, items, true);
|
||||
if (key === null) return null;
|
||||
return { key, label: items.find((it) => it.value === key)?.label ?? "" };
|
||||
}
|
||||
|
||||
/** 집계표 열 — 머리 글(제목행 종류 · 분류1 · 분류2 · 분류3) · 꼴만 씀 */
|
||||
export interface StructureColumn {
|
||||
id: string;
|
||||
@@ -326,28 +354,9 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
|
||||
};
|
||||
|
||||
const pickPrice = async (colId: string, columns: StructureColumn[]): Promise<string | null> => {
|
||||
let list: Awaited<ReturnType<typeof loadCombos>>;
|
||||
try {
|
||||
list = await loadCombos();
|
||||
} catch (error) {
|
||||
showToast(fill(TEXT.combosFailed, why(error)), "error");
|
||||
return null;
|
||||
}
|
||||
const col = columns.find((c) => c.id === colId);
|
||||
const items = list.map((c) => ({
|
||||
value: c.키,
|
||||
key: c.키,
|
||||
// 이름에 단위가 이미 붙은 조합이 있음 — 겹치면 뺌
|
||||
label: [
|
||||
c.이름,
|
||||
c.단위 && !c.이름.includes(`(${c.단위})`) && `(${c.단위})`,
|
||||
c.구분,
|
||||
`· ${LAYER_WORD[c.layer]}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
}));
|
||||
return choose(col ? `${TEXT.pickPrice} — ${columnHead(col)}` : TEXT.pickPrice, items, true);
|
||||
const got = await pickCombo(col ? `${TEXT.pickPrice} — ${columnHead(col)}` : TEXT.pickPrice);
|
||||
return got ? got.key : null;
|
||||
};
|
||||
|
||||
/** 다른 열의 도면 · 면적산출 · 재료산출을 골라 내용만 복사 — 덮어쓰기 확인 · 보고 있던 열이면 다시 읽음 */
|
||||
|
||||
@@ -246,7 +246,8 @@ def apply(
|
||||
made = [*where, _name(body.get("이름"))]
|
||||
_taken_item(out, made)
|
||||
col: dict[str, Any] = {"id": _fresh_id(out), "단위": body.get("단위"), "꼴": "수"}
|
||||
col.update({"일위대가": None, "자재조합": None})
|
||||
# 항목 추가 창에서 고른 일위대가(PLAN 31-2) — 없으면 비움 · 단위는 안 받아도 됨
|
||||
col.update({"일위대가": body.get("일위대가") or None, "자재조합": None})
|
||||
_set_path(col, made)
|
||||
_place(out, col)
|
||||
note.update({"경로": made, "열": col["id"]})
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
type TreeRef,
|
||||
type TreeView,
|
||||
} from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { askFolder, askText } from "./M02_MasterTemplete_TreePanel_Dialogs";
|
||||
import { askFolder, askNewItem, askText } from "./M02_MasterTemplete_TreePanel_Dialogs";
|
||||
import {
|
||||
clipOf,
|
||||
keepClip,
|
||||
@@ -82,6 +82,8 @@ export interface TreePanelOptions {
|
||||
};
|
||||
/** 복사해 둔 것의 나무 종류 — 같은 종류 나무끼리만 붙임(기본 「구조물」) */
|
||||
clipKind?: string;
|
||||
/** 항목 추가 창의 일위대가 고르기(PLAN 31-2 · 세 층 조합 · 찾기 AND) — 없으면 그 줄 없음 */
|
||||
pickCombo?: () => Promise<{ key: string; label: string } | null>;
|
||||
/** 「〇〇로 복사」 — 고른 것을 그 층 맨 위에 바로(층을 안 바꿈 · `label` 의 {n} = 개수 · `api.paste` 로) */
|
||||
copyTo?: { layer: Layer; owner: string | null; label: string }[];
|
||||
/** 이 층을 고칠 수 있나 — 아니면 메뉴 · 키 없음 */
|
||||
@@ -493,16 +495,20 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle {
|
||||
async function addItem(parent: string[]): Promise<void> {
|
||||
if (!(await ready())) return;
|
||||
if (opt.item?.onAdd) return opt.item.onAdd(parent);
|
||||
const got = await askText(L("M02_TreeAddItem"), [
|
||||
{ key: "name", label: L("M02_TreeItemName") },
|
||||
{ key: "unit", label: L("M02_TreeUnit") },
|
||||
]);
|
||||
// 종류(맨 위 폴더 · 고른 폴더면 그 안) + 이름 + 일위대가 · 단위 안 받음(PLAN 31-2)
|
||||
const kinds = view ? buildFolders(view).folders.map((f) => f.path[0]) : [];
|
||||
const got = await askNewItem({
|
||||
title: L("M02_TreeAddItem"),
|
||||
kinds,
|
||||
at: parent,
|
||||
pickCombo: opt.pickCombo,
|
||||
});
|
||||
if (got) {
|
||||
await edit({
|
||||
op: "add-item",
|
||||
경로: parent,
|
||||
경로: got.path,
|
||||
이름: got.name,
|
||||
...(got.unit ? { 단위: got.unit } : {}),
|
||||
...(got.combo ? { 일위대가: got.combo } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,107 @@ export function askText(
|
||||
});
|
||||
}
|
||||
|
||||
/** 항목 추가 창이 돌려줌 — `path` = 넣을 폴더(종류 · 고른 폴더) · `combo` = 고른 일위대가 키(없으면 null) */
|
||||
export interface NewItem {
|
||||
path: string[];
|
||||
name: string;
|
||||
combo: string | null;
|
||||
}
|
||||
|
||||
/** 항목 추가(PLAN 31-2) — 구조물 종류(맨 위 폴더 · 있는 것 고르거나 새 이름 · 나무에서 폴더를 골라 두었으면
|
||||
* 그 안이라 안 물음) + 이름 + 일위대가(`pickCombo` 고르기 창 · 비워도 됨 — 표에서 나중에) · 단위는 안 받음 */
|
||||
export function askNewItem(opt: {
|
||||
title: string;
|
||||
/** 있는 종류(맨 위 폴더 이름) — 칸에 글을 치면 목록에서 고름 */
|
||||
kinds: string[];
|
||||
/** 나무에서 골라 둔 폴더 — 있으면 종류 칸 없음 */
|
||||
at: string[];
|
||||
pickCombo?: () => Promise<{ key: string; label: string } | null>;
|
||||
}): Promise<NewItem | null> {
|
||||
return new Promise((resolve) => {
|
||||
let done = false;
|
||||
const finish = (value: NewItem | null): void => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
resolve(value);
|
||||
};
|
||||
openModal({
|
||||
title: opt.title,
|
||||
closeLabel: L("M02_Close"),
|
||||
dialogClass: "m02-modal m02-treedlg",
|
||||
beforeClose: () => (finish(null), true),
|
||||
mount: (body, close) => {
|
||||
const kind = opt.at.length ? null : createInputField({ label: L("M02_NewItemKind") });
|
||||
if (kind) {
|
||||
const list = el("datalist", {
|
||||
attrs: { id: `m02-kinds-${Date.now()}` },
|
||||
children: opt.kinds.map((k) => el("option", { attrs: { value: k } })),
|
||||
});
|
||||
kind.input.setAttribute("list", list.id);
|
||||
kind.root.append(list);
|
||||
}
|
||||
const where = opt.at.length
|
||||
? el("p", {
|
||||
className: "m02-treedlg__at",
|
||||
text: L("M02_NewItemAt").replace("{value}", opt.at.join(" / ")),
|
||||
})
|
||||
: null;
|
||||
const name = createInputField({ label: L("M02_TreeItemName") });
|
||||
let combo: { key: string; label: string } | null = null;
|
||||
const shown = el("span", {
|
||||
className: "m02-treedlg__combo",
|
||||
text: L("M02_NewItemNoCombo"),
|
||||
});
|
||||
const pick = createButton({
|
||||
label: L("M02_NewItemPickCombo"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
});
|
||||
pick.addEventListener("click", async () => {
|
||||
const got = await opt.pickCombo?.();
|
||||
if (got === null || got === undefined) return; // 닫음 = 그대로
|
||||
combo = got.key ? got : null; // 「비움」 = 없앰
|
||||
shown.textContent = combo ? `${combo.key} ${combo.label}` : L("M02_NewItemNoCombo");
|
||||
});
|
||||
const comboRow = opt.pickCombo
|
||||
? el("div", {
|
||||
className: "m02-treedlg__row",
|
||||
children: [el("span", { text: L("M02_NewItemCombo") }), shown, pick],
|
||||
})
|
||||
: null;
|
||||
const ok = createButton({ label: L("M02_TreeOk"), variant: "filled" });
|
||||
const need = (field: typeof name, label: string): boolean => {
|
||||
const empty = !field.input.value.trim();
|
||||
field.setError(empty ? L("M02_FieldNeeded").replace("{label}", label) : undefined);
|
||||
return empty;
|
||||
};
|
||||
const submit = (): void => {
|
||||
// 빈 칸은 그 칸에 빨간 테두리 + 한 줄(칸마다)
|
||||
const badKind = kind ? need(kind, L("M02_NewItemKind")) : false;
|
||||
const badName = need(name, L("M02_TreeItemName"));
|
||||
if (badKind || badName) return;
|
||||
const path = kind ? [kind.input.value.trim()] : opt.at;
|
||||
finish({ path, name: name.input.value.trim(), combo: combo?.key ?? null });
|
||||
close();
|
||||
};
|
||||
ok.addEventListener("click", submit);
|
||||
for (const field of [kind, name]) {
|
||||
if (!field) continue;
|
||||
field.input.addEventListener("input", () => field.setError(undefined));
|
||||
field.input.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" && !event.isComposing) submit();
|
||||
});
|
||||
}
|
||||
body.append(
|
||||
...[where, kind?.root, name.root, comboRow].filter((n): n is HTMLDivElement => !!n),
|
||||
ok,
|
||||
);
|
||||
(kind ?? name).input.focus();
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 옮길 폴더 고르기 — `folders` 에서 `blocked` 인 것은 뺌 · 맨 위(폴더 밖)는 늘 있음 */
|
||||
export function askFolder(
|
||||
title: string,
|
||||
|
||||
@@ -161,11 +161,24 @@ export function createMain(
|
||||
const root = column;
|
||||
const isCombo = (at: Selection | null): boolean =>
|
||||
!!at && at.kind === "table" && at.name === STRUCTURE_TABLE && at.layer !== "project";
|
||||
// 찾기 칸 · 「새 항목」은 「구조물 조합」일 때만
|
||||
// 찾기 칸 · 「새 항목」 · 표 접기(제목)는 「구조물 조합」일 때만
|
||||
const showAside = (): void => {
|
||||
if (extras.search) extras.search.hidden = !isCombo(sel);
|
||||
if (extras.newItem) extras.newItem.hidden = !isCombo(sel);
|
||||
const combo = isCombo(sel);
|
||||
if (extras.search) extras.search.hidden = !combo;
|
||||
if (extras.newItem) extras.newItem.hidden = !combo;
|
||||
title.classList.toggle("m02-main__title--fold", combo);
|
||||
title.title = combo ? L("M02_TableFold") : "";
|
||||
if (!combo) {
|
||||
host.hidden = false; // 다른 상세(도면 · 면적 · 재료)는 늘 펼침
|
||||
title.classList.remove("is-folded");
|
||||
}
|
||||
};
|
||||
// 「구조물 조합」 표 접기(PLAN 31-2) — 제목을 누르면 표만 접힘 · 펴짐(카드 · 찾기는 그대로)
|
||||
title.addEventListener("click", () => {
|
||||
if (!isCombo(sel)) return;
|
||||
host.hidden = !host.hidden;
|
||||
title.classList.toggle("is-folded", host.hidden);
|
||||
});
|
||||
|
||||
let sel: Selection | null = null;
|
||||
let version = "";
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
type Root,
|
||||
} from "./M02_MasterTemplete_Layer";
|
||||
import { buildLaborLibrary, buildLaborSection } from "./M02_MasterTemplete_Labor";
|
||||
import { STRUCTURE_TABLE } from "./M02_MasterTemplete_Structure";
|
||||
import { STRUCTURE_TABLE, pickCombo } from "./M02_MasterTemplete_Structure";
|
||||
import { createTreePanel } from "./M02_MasterTemplete_TreePanel";
|
||||
import {
|
||||
companyOwner,
|
||||
@@ -75,6 +75,7 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise<Mounte
|
||||
layer,
|
||||
owner,
|
||||
copyTo,
|
||||
pickCombo: () => pickCombo(L("M02_TreePickCombo")), // 항목 추가 창 일위대가(31-2)
|
||||
canEdit: canEdit(layer, isAdmin),
|
||||
onPick: (item) => void main.openTable(layer, item.열),
|
||||
settle: () => main.settle(),
|
||||
|
||||
@@ -242,3 +242,25 @@
|
||||
.m02-main__new[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 「구조물 조합」 제목 = 표 접기 · 펴기(PLAN 31-2) — 캐럿은 공용 접기 컨테이너와 같은 모양 */
|
||||
.m02-main__title--fold {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.m02-main__title--fold::after {
|
||||
content: "▾";
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.m02-main__title--fold.is-folded::after {
|
||||
content: "▸";
|
||||
}
|
||||
|
||||
.m02-main__host[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ def test_나무는_좌측_패널_찾기_칸은_상세_위쪽():
|
||||
assert 'at.kind === "table" && at.name === STRUCTURE_TABLE && at.layer !== "project"' in main
|
||||
assert 'isCombo(next) ? L("M02_KindTable")' in main # 상세 제목
|
||||
assert main.count("showAside();") == 3 # 열 때 · 양식 밖 화면 · 처음
|
||||
assert "if (extras.search) extras.search.hidden = !isCombo(sel);" in main
|
||||
assert "if (extras.search) extras.search.hidden = !combo;" in main
|
||||
locale = _text(ROOT / "ui_template" / "ui_template_locale_m2.ts")
|
||||
assert 'M02_KindTable: ["구조물 조합"' in locale
|
||||
assert ".m02-shell" not in _text(
|
||||
|
||||
@@ -244,7 +244,7 @@ def test_구조물_조합_제목_줄_새_항목():
|
||||
"children: [title, back, slot, reload, save, ...(extras.newItem ? [extras.newItem] : [])]"
|
||||
in main
|
||||
)
|
||||
assert "if (extras.newItem) extras.newItem.hidden = !isCombo(sel);" in main
|
||||
assert "if (extras.newItem) extras.newItem.hidden = !combo;" in main
|
||||
page = _text(M02 / "M02_MasterTemplete_UI_Page.ts")
|
||||
assert (
|
||||
"...(canEdit(layer, isAdmin) ? { newItem: newItemButton(() => tree.newItem()) } : {})"
|
||||
|
||||
@@ -107,3 +107,37 @@ def test_여러_개_옮기기_지우기_한_번에(world):
|
||||
note = t.do(op="delete", 대상=[{"경로": ["나"], "키": one}, {"경로": ["나", "가"]}])
|
||||
assert len(note["알림"]["지운항목"]) == 2
|
||||
assert t.paths() == [["나", "둘"]]
|
||||
|
||||
|
||||
def test_항목_추가_일위대가_단위_없이(world):
|
||||
"""PLAN 31-2 — 항목 추가 = 종류(폴더) + 이름 + 일위대가 · 단위 안 받아도 됨."""
|
||||
t = Tree(_as(world), "personal")
|
||||
t.do(op="add-item", 경로=["옹벽"], 이름="석축", 일위대가="UA000001")
|
||||
made = t.item(["옹벽", "석축"])
|
||||
assert made["일위대가"] == "UA000001" and made["단위"] is None
|
||||
t.do(op="add-item", 경로=["옹벽"], 이름="빈") # 일위대가 비워도 됨
|
||||
assert t.item(["옹벽", "빈"])["일위대가"] is None
|
||||
|
||||
|
||||
def test_항목_추가_창_카드_격자_표_접기():
|
||||
"""PLAN 31-2 화면 — 창 = 종류(있는 것 · 새 이름 · 고른 폴더면 안 물음) + 이름 + 일위대가 ·
|
||||
단위 칸 없음 · 카드 2행 2열 · 높이 고정 · 넘치면 카드 · 상세 칸 스크롤 · 제목 눌러 표 접힘."""
|
||||
from pathlib import Path
|
||||
|
||||
m02 = Path(__file__).resolve().parents[2] / "M02_MasterTemplete"
|
||||
dlg = (m02 / "M02_MasterTemplete_TreePanel_Dialogs.ts").read_text("utf-8")
|
||||
assert (
|
||||
"export function askNewItem(" in dlg and 'kind.input.setAttribute("list", list.id);' in dlg
|
||||
)
|
||||
assert "const kind = opt.at.length ? null :" in dlg and "M02_TreeUnit" not in dlg
|
||||
panel = (m02 / "M02_MasterTemplete_TreePanel.ts").read_text("utf-8")
|
||||
assert "...(got.combo ? { 일위대가: got.combo } : {})," in panel
|
||||
assert "askNewItem({" in panel and "M02_TreeUnit" not in panel
|
||||
css = (m02 / "M02_MasterTemplete_Structure.css").read_text("utf-8")
|
||||
assert "grid-template-columns: repeat(2, minmax(0, 1fr));" in css
|
||||
assert (
|
||||
"grid-auto-rows: var(--m02-struct-card-h);" in css and "--m02-struct-card-h: 360px;" in css
|
||||
)
|
||||
assert "max-height: calc(100vh - var(--spacing-64));\n overflow-y: auto;" in css
|
||||
main = (m02 / "M02_MasterTemplete_UI_Main.ts").read_text("utf-8")
|
||||
assert "host.hidden = !host.hidden;" in main and 'title.classList.toggle("is-folded"' in main
|
||||
|
||||
@@ -34,12 +34,13 @@ def test_항목_더하기_빈_칸은_칸_안내로_알림():
|
||||
assert "showToast" not in dialogs # 창 뒤에 가려 안 보이던 토스트 안 씀
|
||||
locale = _text("ui_template/ui_template_locale_m2.ts")
|
||||
assert re.search(r'M02_FieldNeeded: \["\{label\}', locale)
|
||||
# 단위도 비우면 안내 — 옵션(optional)이 아님
|
||||
# 항목 추가(PLAN 31-2) = 종류 · 이름 비우면 칸 안내 · 단위 칸은 없음(31 정함)
|
||||
new_item = dialogs.split("export function askNewItem(")[1].split("export function askFolder")[0]
|
||||
assert 'L("M02_FieldNeeded").replace("{label}", label)' in new_item
|
||||
assert "need(kind, " in new_item and "need(name, " in new_item
|
||||
panel = _text("M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts")
|
||||
assert (
|
||||
'label: L("M02_TreeUnit") }' in panel
|
||||
and "optional: true"
|
||||
not in panel.split("async function addItem")[1].split("async function rename")[0]
|
||||
"askNewItem({" in panel.split("async function addItem")[1].split("async function rename")[0]
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -132,6 +132,16 @@ export const ui_locales_m2 = {
|
||||
],
|
||||
M02_TreePicked: ["{n}개 고름", "{n} selected"],
|
||||
M02_TreeNewItem: ["새 항목", "New item"],
|
||||
M02_NewItemKind: ["구조물 종류", "Structure type"],
|
||||
M02_NewItemAt: ["넣을 곳: {value}", "Into: {value}"],
|
||||
M02_NewItemCombo: ["일위대가", "Unit price"],
|
||||
M02_NewItemNoCombo: [
|
||||
"없음 — 표에서 나중에 골라도 됨",
|
||||
"None — you can pick it later in the table",
|
||||
],
|
||||
M02_NewItemPickCombo: ["고르기", "Pick"],
|
||||
M02_TreePickCombo: ["일위대가 고르기", "Pick unit price"],
|
||||
M02_TableFold: ["표 접기 · 펴기", "Fold or unfold the table"],
|
||||
M02_TreeCopyToMaster: ["마스터로 복사 ({n}개)", "Copy to master ({n})"],
|
||||
M02_TreeCopyToCompany: ["회사로 복사 ({n}개)", "Copy to company ({n})"],
|
||||
M02_TreeCopyToPersonal: ["개인으로 복사 ({n}개)", "Copy to personal ({n})"],
|
||||
|
||||
Reference in New Issue
Block a user