diff --git a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts index 281ee93c..0ef68876 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts @@ -447,6 +447,8 @@ export interface TreeEdit { 새경로?: string[]; 키?: string; 단위?: string; + /** 항목 추가 때 고른 일위대가 조합 키(PLAN 31-2) */ + 일위대가?: string; } /** `owner` = 남의 층(없으면 자기 층) — 지금 층과 따로 받음(나무 부품이 여러 뿌리를 그림) */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py index 296d339f..4321aee0 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py @@ -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 diff --git a/M02_MasterTemplete/M02_MasterTemplete_Structure.css b/M02_MasterTemplete/M02_MasterTemplete_Structure.css index 864c2011..0653bd1f 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Structure.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Structure.css @@ -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 속성을 덮어씀 — 안 보일 땐 확실히 안 보이게 */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts index 52ae9568..14510a77 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts @@ -166,6 +166,34 @@ function choose(title: string, items: Choice[], clearable: boolean): Promise { + let list: Awaited>; + 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 => { - let list: Awaited>; - 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; }; /** 다른 열의 도면 · 면적산출 · 재료산출을 골라 내용만 복사 — 덮어쓰기 확인 · 보고 있던 열이면 다시 읽음 */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Tree.py b/M02_MasterTemplete/M02_MasterTemplete_Tree.py index 32d16e1a..d051f948 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Tree.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Tree.py @@ -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"]}) diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts index b82b5df4..4fa07239 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts @@ -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 { 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 } : {}), }); } } diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Dialogs.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Dialogs.ts index 2eb6969f..4efd9be8 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Dialogs.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Dialogs.ts @@ -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 { + 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, diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index e6f0783a..9f57664f 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -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 = ""; diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts index c735cae8..b394c3b2 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts @@ -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 pickCombo(L("M02_TreePickCombo")), // 항목 추가 창 일위대가(31-2) canEdit: canEdit(layer, isAdmin), onPick: (item) => void main.openTable(layer, item.열), settle: () => main.settle(), diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css index 6d03e654..0e323ca8 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css @@ -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; +} diff --git a/resources/tester/test_m02_combo_screen.py b/resources/tester/test_m02_combo_screen.py index 74dc2bca..b76347dd 100644 --- a/resources/tester/test_m02_combo_screen.py +++ b/resources/tester/test_m02_combo_screen.py @@ -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( diff --git a/resources/tester/test_m02_layer_screen.py b/resources/tester/test_m02_layer_screen.py index 75eb7c0d..233719bf 100644 --- a/resources/tester/test_m02_layer_screen.py +++ b/resources/tester/test_m02_layer_screen.py @@ -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()) } : {})" diff --git a/resources/tester/test_m02_tree_paste.py b/resources/tester/test_m02_tree_paste.py index 85d2d266..f760708a 100644 --- a/resources/tester/test_m02_tree_paste.py +++ b/resources/tester/test_m02_tree_paste.py @@ -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 diff --git a/resources/tester/test_summary_clear_and_tree_dialog.py b/resources/tester/test_summary_clear_and_tree_dialog.py index 62cbe191..0a27fc23 100644 --- a/resources/tester/test_summary_clear_and_tree_dialog.py +++ b/resources/tester/test_summary_clear_and_tree_dialog.py @@ -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] ) diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index 51e61278..eff4717b 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -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})"],