refactor(M02): 새 항목 일위대가 고르기 없앰 — 항목 추가 창 줄 · pickCombo · 서버가 받는 일위대가 키 · 열 칸 새로 쓰기 · /combos/all · fetchUsableCombos 지움 · 새 구조물 일위대가 = 빈 labor 문서(39-1d)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
* M02_MasterTemplete_Labor_Api.ts
|
||||
* 일위대가 조합 서버 길 — 층(마스터 UA · 회사 UC · 개인 UP) 조합 표 `일위대가조합.json`
|
||||
* 길 = `/api/m02/layers/{layer}/combos`(PLAN 27-2a) · 키 하나 = `/api/m02/combos/key/{키}` ·
|
||||
* 세 층 = `/api/m02/combos/all` · 담을 로직 읽기 = `/api/m02/logic-lib/…` · 모양 = `_화면_계약.md` 9장
|
||||
* 담을 로직 읽기 = `/api/m02/logic-lib/…` · 모양 = `_화면_계약.md` 9장
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
@@ -74,10 +74,6 @@ export interface ComboList {
|
||||
폴더: string[][];
|
||||
줄: ComboSummary[];
|
||||
}
|
||||
export interface ComboLayerList extends ComboList {
|
||||
layer: LaborScope["layer"];
|
||||
owner: string | null;
|
||||
}
|
||||
async function request<T>(url: string, method = "GET", body?: unknown): Promise<T> {
|
||||
const response = await fetch(`${API_BASE_URL}/m02${url}`, {
|
||||
method,
|
||||
@@ -105,26 +101,6 @@ export const fetchComboList = (scope: LaborScope = SYSTEM_SCOPE): Promise<ComboL
|
||||
export const fetchCombos = (scope: LaborScope = SYSTEM_SCOPE): Promise<ComboSummary[]> =>
|
||||
fetchComboList(scope).then((d) => d.줄);
|
||||
|
||||
/** 세 층 목록 — 고르기(use-combo · 갈라 내기) */
|
||||
export const fetchAllCombos = (): Promise<ComboLayerList[]> => request("/combos/all");
|
||||
|
||||
/** 조합 고르기(PLAN 27-2c) — 이 층 항목이 쓸 수 있는 조합: 마스터 · 회사(회사 · 개인 층) · 개인(개인 층) ·
|
||||
* 차례 = 마스터 → 회사 → 개인 · `layer` = 그 조합의 층 */
|
||||
export async function fetchUsableCombos(
|
||||
layer: Layer,
|
||||
): Promise<(ComboSummary & { layer: LaborScope["layer"] })[]> {
|
||||
const usable: LaborScope["layer"][] =
|
||||
layer === "personal" || layer === "project"
|
||||
? ["system", "company", "personal"]
|
||||
: layer === "company"
|
||||
? ["system", "company"]
|
||||
: ["system"];
|
||||
const all = await fetchAllCombos();
|
||||
return all
|
||||
.filter((group) => usable.includes(group.layer))
|
||||
.flatMap((group) => group.줄.map((c) => ({ ...c, layer: group.layer })));
|
||||
}
|
||||
|
||||
/** 키 하나 — 어느 층이든(그 층 읽기 권한) · `version` = 그 층 조합 표 판 */
|
||||
export const fetchCombo = (key: string): Promise<ComboOne & LaborScope> =>
|
||||
request<Omit<ComboOne, "file" | "version"> & LaborScope & { 판: string }>(
|
||||
|
||||
@@ -32,20 +32,6 @@ router = APIRouter(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/combos/all")
|
||||
def get_all(session: Session = Depends(verify_session)) -> list[dict]:
|
||||
"""고르기(use-combo · 갈라 내기) — 세 층 `[{layer, owner, 판, 폴더, 줄}]` · 회사 없음 빈 층."""
|
||||
out = []
|
||||
for layer in ("system", "company", "personal"):
|
||||
who = None if layer == "system" else access.own_owner(session, layer)
|
||||
if layer != "system" and who is None:
|
||||
out.append({"layer": layer, "owner": None, "판": "", "폴더": [], "줄": []})
|
||||
continue
|
||||
got = _call(_root(session, layer, who), combos.listing)
|
||||
out.append({"layer": layer, "owner": who, **got})
|
||||
return out
|
||||
|
||||
|
||||
def _where(key: str) -> tuple[str, str | None]:
|
||||
got = combos.where(key)
|
||||
if got is None:
|
||||
|
||||
@@ -72,7 +72,6 @@ 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
|
||||
|
||||
@@ -9,21 +9,13 @@
|
||||
* 구조물 도면은 집계표 열마다 하나(`resources/master_template/structure/<열 id>.json`) ·
|
||||
* 서버가 열마다 미리 만들어 둠(번호 자동 · 새 열도 집계표 저장 때 서버가 같이 만듦 — `M02_MasterTemplete_Store.py`).
|
||||
* 이 칸은 표 부품 host 밖 형제 자리에 붙음 — 집계표는 그대로 두고 여기만 열마다 다시 그림.
|
||||
* 집계표 일위대가 · 도면 · 면적산출 줄의 조회 후 선택(`pick`)도 여기 — 일위대가 = M01 일위대가
|
||||
* 목록에서 고름 · 나머지 = 다른 열 것을 골라 내용만 복사해 가져옴(번호 · 열은 그대로 · 서버가 복사).
|
||||
* 집계표 일위대가 · 도면 · 면적산출 줄의 조회 후 선택(`pick`)도 여기 — 다른 열 것을 골라 내용만
|
||||
* 복사해 가져옴(번호 · 열은 그대로 · 서버가 복사 · 일위대가 = 열마다 labor 문서 · 39장).
|
||||
* 글자는 이 파일에 둠(도면 부품 `M02_MasterTemplete_Drawing.ts` 와 같은 방식).
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
createButton,
|
||||
createInputField,
|
||||
el,
|
||||
showConfirmDialog,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
||||
import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links";
|
||||
import { fetchUsableCombos } from "./M02_MasterTemplete_Labor_Api";
|
||||
import {
|
||||
copyPartFrom,
|
||||
fetchLaborPills,
|
||||
@@ -53,11 +45,6 @@ export type { CardKind, EditKind, SheetKind } from "./M02_MasterTemplete_ItemCar
|
||||
export const STRUCTURE_TABLE = "구조물집계표";
|
||||
|
||||
const TEXT = {
|
||||
close: "닫기",
|
||||
find: "찾기",
|
||||
clear: "비움",
|
||||
none: "고를 것이 없음",
|
||||
combosFailed: "일위대가 목록을 못 받음 — {value}",
|
||||
saveFirst: "새 열은 구조물 조합을 먼저 저장해야 번호가 생김",
|
||||
overwrite: "{from} 의 {what} 내용을 {to} 에 덮어씀 — 번호 · 열은 그대로",
|
||||
pickCopy: "{value} 복사해 가져오기",
|
||||
@@ -81,114 +68,6 @@ const fill = (text: string, value: string): string => text.replace("{value}", va
|
||||
/** `{열 id: {도번, 면적번}}` — 집계표 번호 줄(없는 열은 키 없음 = 「없음」) */
|
||||
export type ColumnNumbers = Record<string, StructureNumbers>;
|
||||
|
||||
/** 일위대가 목록 — 이 층이 쓸 수 있는 세 층 조합(27-2c) · 고를 때마다 받음(층을 바꿔 다시 들어와도 맞게) */
|
||||
const loadCombos = () => fetchUsableCombos(getActiveLayer());
|
||||
const LAYER_WORD = { system: "마스터", company: "회사", personal: "개인" } as const;
|
||||
|
||||
interface Choice {
|
||||
value: string;
|
||||
key: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/** 찾기 칸 + 목록 창 — 고른 값(「비움」 = 빈 글) · 그냥 닫으면 null */
|
||||
function choose(title: string, items: Choice[], clearable: boolean): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
let done = false;
|
||||
const finish = (value: string | null): void => {
|
||||
if (!done) resolve(value);
|
||||
done = true;
|
||||
};
|
||||
openModal({
|
||||
title,
|
||||
closeLabel: TEXT.close,
|
||||
dialogClass: "m02-modal m02-pick",
|
||||
mount: (body, close) => {
|
||||
const search = createInputField({ placeholder: TEXT.find, type: "search" });
|
||||
const list = el("div", { className: "m02-pick__list" });
|
||||
const paint = (): void => {
|
||||
const words = search.input.value.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
||||
const hits = items.filter((it) =>
|
||||
words.every((w) => `${it.key} ${it.label}`.toLowerCase().includes(w)),
|
||||
);
|
||||
list.replaceChildren(
|
||||
...(hits.length
|
||||
? hits.map((it) =>
|
||||
el("button", {
|
||||
className: "m02-pick__item",
|
||||
attrs: { type: "button", "data-value": it.value },
|
||||
children: [el("span", { className: "m02-pick__key", text: it.key }), it.label],
|
||||
}),
|
||||
)
|
||||
: [el("p", { className: "m02-struct__empty", text: TEXT.none })]),
|
||||
);
|
||||
};
|
||||
list.addEventListener("click", (event) => {
|
||||
const item = (event.target as HTMLElement).closest<HTMLElement>("[data-value]");
|
||||
if (!item) return;
|
||||
finish(item.dataset.value!);
|
||||
close();
|
||||
});
|
||||
search.input.addEventListener("input", paint);
|
||||
search.input.addEventListener("keydown", (event) => {
|
||||
if (event.key !== "Enter") return;
|
||||
list.querySelector<HTMLElement>("[data-value]")?.click(); // 첫 줄
|
||||
});
|
||||
const foot = clearable
|
||||
? [
|
||||
createButton({
|
||||
label: TEXT.clear,
|
||||
variant: "ghost",
|
||||
onClick: () => {
|
||||
finish("");
|
||||
close();
|
||||
},
|
||||
}),
|
||||
]
|
||||
: [];
|
||||
body.append(search.root, list, ...foot);
|
||||
paint();
|
||||
search.input.focus();
|
||||
},
|
||||
});
|
||||
// 닫기 단추 · Esc · 바깥 누르기 — 창이 빠지면 null
|
||||
const watch = new MutationObserver(() => {
|
||||
if (document.querySelector(".m02-pick")) return;
|
||||
watch.disconnect();
|
||||
finish(null);
|
||||
});
|
||||
watch.observe(document.body, { childList: true });
|
||||
});
|
||||
}
|
||||
|
||||
/** 일위대가 고르기 창 — 이 층이 쓸 수 있는 세 층 조합 · 찾기 = 띄어쓰기 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;
|
||||
|
||||
@@ -246,8 +246,7 @@ def apply(
|
||||
made = [*where, _name(body.get("이름"))]
|
||||
_taken_item(out, made)
|
||||
col: dict[str, Any] = {"id": _fresh_id(out), "단위": body.get("단위"), "꼴": "수"}
|
||||
# 항목 추가 창에서 고른 일위대가(PLAN 31-2) — 없으면 비움 · 단위는 안 받아도 됨
|
||||
col["일위대가"] = body.get("일위대가") or None
|
||||
# 일위대가는 열 칸에 안 적음 — 늘 빈 labor 문서로 생기고 카드에서 담음(39-1d)
|
||||
_set_path(col, made)
|
||||
_place(out, col)
|
||||
note.update({"경로": made, "열": col["id"]})
|
||||
|
||||
@@ -82,8 +82,6 @@ export interface TreePanelOptions {
|
||||
dest: string[],
|
||||
) => Promise<TreeView>;
|
||||
};
|
||||
/** 항목 추가 창의 일위대가 고르기(PLAN 31-2 · 세 층 조합 · 찾기 AND) — 없으면 그 줄 없음 */
|
||||
pickCombo?: () => Promise<{ key: string; label: string } | null>;
|
||||
/** 「〇〇로 복사…」 — 그 층 폴더를 골라 바로(층을 안 바꿈 · `label` 의 {n} = 개수 · `api.paste` 로 · 39-6) */
|
||||
copyTo?: CopyTarget[];
|
||||
/** 이 층을 고칠 수 있나 — 아니면 메뉴 · 키 없음 */
|
||||
@@ -465,22 +463,14 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle {
|
||||
|
||||
async function addItem(parent: string[]): Promise<void> {
|
||||
if (!(await ready())) return;
|
||||
// 종류(맨 위 폴더 · 고른 폴더면 그 안) + 이름 + 일위대가 · 단위 안 받음(PLAN 31-2)
|
||||
// 종류(맨 위 폴더 · 고른 폴더면 그 안) + 이름 · 단위 안 받음(PLAN 31-2) · 일위대가 = 빈 문서(39-1d)
|
||||
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",
|
||||
경로: got.path,
|
||||
이름: got.name,
|
||||
...(got.combo ? { 일위대가: got.combo } : {}),
|
||||
});
|
||||
}
|
||||
if (got) await edit({ op: "add-item", 경로: got.path, 이름: got.name });
|
||||
}
|
||||
|
||||
async function rename(target: Target): Promise<void> {
|
||||
|
||||
@@ -68,22 +68,20 @@ export function askText(
|
||||
});
|
||||
}
|
||||
|
||||
/** 항목 추가 창이 돌려줌 — `path` = 넣을 폴더(종류 · 고른 폴더) · `combo` = 고른 일위대가 키(없으면 null) */
|
||||
/** 항목 추가 창이 돌려줌 — `path` = 넣을 폴더(종류 · 고른 폴더) */
|
||||
export interface NewItem {
|
||||
path: string[];
|
||||
name: string;
|
||||
combo: string | null;
|
||||
}
|
||||
|
||||
/** 항목 추가(PLAN 31-2) — 구조물 종류(맨 위 폴더 · 있는 것 고르거나 새 이름 · 나무에서 폴더를 골라 두었으면
|
||||
* 그 안이라 안 물음) + 이름 + 일위대가(`pickCombo` 고르기 창 · 비워도 됨 — 표에서 나중에) · 단위는 안 받음 */
|
||||
* 그 안이라 안 물음) + 이름 · 단위는 안 받음 · 일위대가는 늘 빈 문서로 생기고 카드에서 담음(39-1d) */
|
||||
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;
|
||||
@@ -114,28 +112,6 @@ export function askNewItem(opt: {
|
||||
})
|
||||
: 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();
|
||||
@@ -148,7 +124,7 @@ export function askNewItem(opt: {
|
||||
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 });
|
||||
finish({ path, name: name.input.value.trim() });
|
||||
close();
|
||||
};
|
||||
ok.addEventListener("click", submit);
|
||||
@@ -159,10 +135,7 @@ export function askNewItem(opt: {
|
||||
if (event.key === "Enter" && !event.isComposing) submit();
|
||||
});
|
||||
}
|
||||
body.append(
|
||||
...[where, kind?.root, name.root, comboRow].filter((n): n is HTMLDivElement => !!n),
|
||||
ok,
|
||||
);
|
||||
body.append(...[where, kind?.root, name.root].filter((n): n is HTMLDivElement => !!n), ok);
|
||||
(kind ?? name).input.focus();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
} from "./M02_MasterTemplete_Layer";
|
||||
import {
|
||||
headRenameEdit,
|
||||
pickCombo,
|
||||
STRUCTURE_TABLE,
|
||||
type StructureColumn,
|
||||
} from "./M02_MasterTemplete_Structure";
|
||||
@@ -95,7 +94,6 @@ 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(),
|
||||
|
||||
@@ -63,7 +63,6 @@ def test_조합_길은_시스템_관리자만(world):
|
||||
for role in ("USER", "ADMIN"):
|
||||
client = _as(world, role)
|
||||
assert client.get(URL.format("system")).status_code == 403
|
||||
assert client.get("/api/m02/combos/all").status_code == 403
|
||||
assert client.get("/api/m02/combos/key/UA000001").status_code == 403
|
||||
assert client.get("/api/m02/logic-lib/logics").status_code == 200
|
||||
world["session"]["is_master"] = True
|
||||
@@ -190,9 +189,7 @@ def test_세_층_목록_고르기_남의_층_403_갈라_낸_것은_그_층에(wo
|
||||
uc = _new(boss, "company", _combo("회사 조합", (A, B)))["키"]
|
||||
me = _as(world)
|
||||
up = _new(me, "personal", _combo("내 조합"))["키"]
|
||||
all_ = me.get("/api/m02/combos/all").json()
|
||||
assert [g["layer"] for g in all_] == ["system", "company", "personal"]
|
||||
assert [r["키"] for r in all_[1]["줄"]] == [uc] and [r["키"] for r in all_[2]["줄"]] == [up]
|
||||
assert me.get("/api/m02/combos/all").status_code == 404 # 세 층 고르기 길 은퇴(39-1d)
|
||||
assert combos.find(uc)["이름"] == "회사 조합" and combos.find(up)["이름"] == "내 조합"
|
||||
assert combos.find("UP42-999999") is None and combos.find("엉터리") is None
|
||||
by_key = me.get(f"/api/m02/combos/key/{up}").json()
|
||||
|
||||
@@ -110,18 +110,22 @@ def test_여러_개_옮기기_지우기_한_번에(world):
|
||||
|
||||
|
||||
def test_항목_추가_일위대가_단위_없이(world):
|
||||
"""PLAN 31-2 — 항목 추가 = 종류(폴더) + 이름 + 일위대가 · 단위 안 받아도 됨."""
|
||||
"""PLAN 31-2 · 39-1d — 항목 추가 = 종류(폴더) + 이름 · 단위 안 받아도 됨 ·
|
||||
일위대가 키는 안 받음 — 열 칸에 안 적고 늘 빈 labor 문서
|
||||
(담기는 카드 [새로 만들기] · [복사 생성])."""
|
||||
t = Tree(_as(world), "personal")
|
||||
t.do(op="add-item", 경로=["옹벽"], 이름="석축", 일위대가="UA000001")
|
||||
note = 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
|
||||
assert not made.get("일위대가") and made["단위"] is None # 보낸 키는 버림
|
||||
labor = world["storage"] / "7" / "42" / "templates" / "labor" / f"{note['알림']['열']}.json"
|
||||
doc = json.loads(labor.read_text(encoding="utf-8"))
|
||||
assert doc["담은로직"] == [] and doc["자재"] == [] and doc["일위대가번"].startswith("SU")
|
||||
|
||||
|
||||
def test_항목_추가_창_카드_격자_표_접기():
|
||||
"""PLAN 31-2 화면 — 창 = 종류(있는 것 · 새 이름 · 고른 폴더면 안 물음) + 이름 + 일위대가 ·
|
||||
단위 칸 없음 · 카드 2행 2열 · 높이 고정 · 넘치면 카드 · 상세 칸 스크롤 · 제목 눌러 표 접힘."""
|
||||
"""PLAN 31-2 화면 — 창 = 종류(있는 것 · 새 이름 · 고른 폴더면 안 물음) + 이름 ·
|
||||
일위대가 줄 없음(39-1d) · 단위 칸 없음 · 카드 2행 2열 · 높이 고정 · 넘치면 카드 ·
|
||||
상세 칸 스크롤 · 제목 눌러 표 접힘."""
|
||||
from pathlib import Path
|
||||
|
||||
m02 = Path(__file__).resolve().parents[2] / "M02_MasterTemplete"
|
||||
@@ -131,7 +135,7 @@ def test_항목_추가_창_카드_격자_표_접기():
|
||||
)
|
||||
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 "일위대가:" not in panel and "pickCombo" not in panel + dlg # 39-1d — 고르기 줄 없음
|
||||
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
|
||||
|
||||
@@ -134,13 +134,6 @@ export const ui_locales_m2 = {
|
||||
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})"],
|
||||
|
||||
Reference in New Issue
Block a user