diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts index 0edbf64a3..7c4de288a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts @@ -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(url: string, method = "GET", body?: unknown): Promise { const response = await fetch(`${API_BASE_URL}/m02${url}`, { method, @@ -105,26 +101,6 @@ export const fetchComboList = (scope: LaborScope = SYSTEM_SCOPE): Promise => fetchComboList(scope).then((d) => d.줄); -/** 세 층 목록 — 고르기(use-combo · 갈라 내기) */ -export const fetchAllCombos = (): Promise => 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 => request & LaborScope & { 판: string }>( diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py index 6cc306192..4dfbcc95f 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py @@ -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: diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py index 3eeb6488f..ceaf54a23 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py @@ -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 diff --git a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts index 7dbae86b7..e03126161 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts @@ -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; -/** 일위대가 목록 — 이 층이 쓸 수 있는 세 층 조합(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 { - 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("[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("[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>; - 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; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Tree.py b/M02_MasterTemplete/M02_MasterTemplete_Tree.py index 0a0c5172b..7e3d0369e 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Tree.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Tree.py @@ -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"]}) diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts index 774500c80..99402a371 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts @@ -82,8 +82,6 @@ export interface TreePanelOptions { dest: string[], ) => Promise; }; - /** 항목 추가 창의 일위대가 고르기(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 { 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 { diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Dialogs.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Dialogs.ts index 4efd9be89..ab090480f 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Dialogs.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Dialogs.ts @@ -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 { 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(); }, }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts index bb62f06db..b41320543 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts @@ -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 pickCombo(L("M02_TreePickCombo")), // 항목 추가 창 일위대가(31-2) canEdit: canEdit(layer, isAdmin), onPick: (item) => void main.openTable(layer, item.열), settle: () => main.settle(), diff --git a/resources/tester/test_m02_combo_layers.py b/resources/tester/test_m02_combo_layers.py index 3eab7b262..18b877a24 100644 --- a/resources/tester/test_m02_combo_layers.py +++ b/resources/tester/test_m02_combo_layers.py @@ -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() diff --git a/resources/tester/test_m02_tree_paste.py b/resources/tester/test_m02_tree_paste.py index 56a3d2a72..67b2786eb 100644 --- a/resources/tester/test_m02_tree_paste.py +++ b/resources/tester/test_m02_tree_paste.py @@ -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 diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index 3eba5f311..670d97500 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -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})"],