From a0c2db507f587b12082e7f5fe22f8cafddb7340d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 1 Nov 2026 21:25:12 +0900 Subject: [PATCH] =?UTF-8?q?feat(720-3):=20=EB=8B=A8=EA=B0=80=EC=82=B0?= =?UTF-8?q?=EC=B6=9C=20=C2=B7=20=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20?= =?UTF-8?q?=EB=82=98=EB=AC=B4=20=EB=AC=B8=EC=84=9C=20=EC=A4=84=20[?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B0=94=EA=BE=B8=EA=B8=B0]=20=E2=80=94?= =?UTF-8?q?=20=EC=9D=BD=EA=B3=A0=20=EC=9D=B4=EB=A6=84=EB=A7=8C=20=EA=B0=88?= =?UTF-8?q?=EC=95=84=20=EC=A0=80=EC=9E=A5=20=C2=B7=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EB=92=A4=20=EC=83=81=EC=84=B8=20=EB=A8=B8=EB=A6=AC=20=EC=A0=9C?= =?UTF-8?q?=EB=AA=A9=20=EC=83=88=20=EC=9D=B4=EB=A6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 열린 문서면 저장 안 한 고침 물음 뒤 새 이름으로 다시 엶 · 서버 새 길 없음 - 새 지킴 시험 test_720_m02_folder_layer(임시 storage 이름 바꾸기 왕복 · 화면 글 대조) Co-Authored-By: Claude Opus 5.5 --- .../M02_MasterTemplete_UI_Main.ts | 4 + .../M02_MasterTemplete_UI_Side_Library.ts | 32 ++++++++ resources/tester/test_720_m02_folder_layer.py | 76 +++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 resources/tester/test_720_m02_folder_layer.py diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index 8a9182d22..33105a4ce 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -595,6 +595,10 @@ export function createMain( sheet.recalc(); panel.refresh(sheet.getDoc()?.열 ?? []); } + // 720-3 — 서고 문서(단가산출 · 일위대가) 이름을 고쳐 저장하면 머리 제목도 새 이름(「키 이름」) + const named = (doc as { 이름?: unknown } | null)?.이름; + if ((at.kind === "sangeun" || isPrecombo(at.kind)) && typeof named === "string" && sel === at) + title.textContent = at.label = named ? `${at.name} ${named}` : at.name; showNotice([]); showToast(L("M02_Saved"), "success"); onSaved?.(); // 좌측 목록의 판을 새 판으로 diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Library.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Library.ts index 483834e96..5bdbad680 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Library.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Library.ts @@ -9,6 +9,7 @@ * Delete 가 고른 것 전부에 · 복사는 읽을 수 있는 층 어디서나 → 쓸 수 있는 층에 붙여넣기(새 키). * 문서 누름 = 편집 칸 · 켜진 층 단추 다시 누름 = 목록표(페이지 몫). * 최상위 폴더(720-2) — 층 단추 옆 「+」(맨 위 메뉴) · 폴더 · 문서 줄 [최상위 폴더 만들기] = 같은 `addFolder([])`. + * 문서 줄 [이름 바꾸기](720-3) — 읽고 문서 `이름` 만 갈아 저장(서버 새 길 없음) · 열린 문서면 새 이름으로 다시 엶. * ========================================================================== */ import { isCtrlKey } from "../A00_Common/key_shortcut"; @@ -29,6 +30,8 @@ import { listTemplates, newLibrary, pasteLibraryTree, + readTemplate, + saveTemplate, type Layer, type LibraryKind, type PrecomboTree, @@ -219,6 +222,34 @@ export function buildLibrarySection(opt: LibraryOptions): LibraryHandle { if (got && got.name !== was) await edit({ op: "rename", 경로: path, 이름: got.name }); }; + /** 문서 이름 바꾸기(720-3) — 열린 문서면 저장 안 한 고침 물음 → 읽고 `이름` 만 갈아 저장 → 나무 · 상세 새 이름 */ + const renameDoc = async (row: LibraryRow): Promise => { + const at = opt.scope(); + const was = row.제목 || row.이름; + const got = await askText(L("M02_TreeRename"), [ + { key: "name", label: L("M02_TreeItemName"), value: was }, + ]); + const name = got?.name.trim(); + if (!name || name === was) return; + const cur = opt.getOpen(); + const shown = + cur?.kind === kind && + cur.name === row.이름 && + cur.layer === at.layer && + (cur.owner ?? null) === at.owner; + if (shown && !(await opt.confirmLeave())) return; + try { + const doc = await readTemplate(at.layer, kind, row.이름, null, at.owner); + const body = { ...(doc.문서 as Record), 이름: name }; + await saveTemplate(at.layer, kind, row.이름, null, doc.판 ?? "", body, at.owner); + showToast(L("M02_TreeDone"), "success"); + await refresh(true); + if (shown) await open({ ...row, 제목: name }); + } catch (error) { + showToast(L("M02_ActionFailed").replace("{value}", why(error)), "error"); + } + }; + /* --- 지우기 · 여럿 · 복사 붙여넣기(구조물 조합 나무 꼴) --- */ const pickedTargets = (): Target[] => [...picked].map((id) => targets.get(id)).filter((t): t is Target => !!t); @@ -302,6 +333,7 @@ export function buildLibrarySection(opt: LibraryOptions): LibraryHandle { const off = (): string | undefined => (opt.scope().canWrite ? undefined : tc("ReadOnly")); const single = (): TreeMenuItem[] => [ { label: L("M02_TreeAddTopFolder"), run: () => void addFolder([]), disabledReason: off() }, + { label: L("M02_TreeRename"), run: () => void renameDoc(row), disabledReason: off() }, { label: L("M02_TreeCopy").replace("{n}", "1"), run: () => copy([target]) }, { label: L("M02_Delete"), diff --git a/resources/tester/test_720_m02_folder_layer.py b/resources/tester/test_720_m02_folder_layer.py new file mode 100644 index 000000000..a3b9fddbc --- /dev/null +++ b/resources/tester/test_720_m02_folder_layer.py @@ -0,0 +1,76 @@ +"""M02 폴더 · 층 · 단가산출 이름(PLAN 720 · 내역 16) — 임시 storage + 화면 글 대조. + +720-1 단가산출 · 일위대가 층 단추 = 저장 안 한 고침 물음 → 그 층 목록표 · +720-2 최상위 폴더 = 메뉴 줄 · 층 줄 「+」 같은 `addFolder([])` · +720-3 문서 [이름 바꾸기] = 읽고 `이름` 만 갈아 저장(층 목록 제목 새 이름 · 다른 칸 그대로) · +720-4 구조물 조합 층 = 제자리 바꿈(해시 이동 없음). +""" + +from pathlib import Path + +from fastapi.testclient import TestClient + +from resources.tester.test_m02_precombo_folders import client # noqa: F401 — 같은 임시 층 + +M02 = Path(__file__).resolve().parents[2] / "M02_MasterTemplete" +ITEM = "/api/m02/layers/personal/templates/{}/{}" + + +def _text(name: str) -> str: + return (M02 / name).read_text(encoding="utf-8") + + +def _rows(c: TestClient, kind: str) -> dict[str, dict]: + rows = c.get("/api/m02/layers/personal/templates").json()["양식"] + return {r["이름"]: r for r in rows if r["종류"] == kind} + + +def _rename(c: TestClient, kind: str, key: str, name: str) -> None: + """화면 `renameDoc` 과 같은 길 — 읽고 `이름` 만 갈아 같은 판으로 저장""" + got = c.get(ITEM.format(kind, key)).json() + body = {"판": got.get("판") or "", "문서": {**got["문서"], "이름": name}} + res = c.put(ITEM.format(kind, key), json=body) + assert res.status_code == 200, res.text + + +def test_일위대가_이름_바꾸기는_이름만(client: TestClient): # noqa: F811 + before = client.get(ITEM.format("precombo", "SC-000002")).json()["문서"] + _rename(client, "precombo", "SC-000002", "나 고침") + after = client.get(ITEM.format("precombo", "SC-000002")).json()["문서"] + assert after["이름"] == "나 고침" + assert {k: v for k, v in after.items() if k != "이름"} == { + k: v for k, v in before.items() if k != "이름" + } + rows = _rows(client, "precombo") + assert rows["SC-000002"]["제목"] == "나 고침" and rows["SC-000001"]["제목"] == "가" + + +def test_단가산출_이름_바꾸기_목록_제목(client: TestClient): # noqa: F811 + body = {"문서": {"이름": "갑", "단위": "㎡", "바탕": None, "더한줄": []}} + res = client.post("/api/m02/layers/personal/library/sangeun", json=body) + assert res.status_code == 200, res.text + key = res.json()["이름"] + _rename(client, "sangeun", key, "을") + assert _rows(client, "sangeun")[key]["제목"] == "을" + assert client.get(ITEM.format("sangeun", key)).json()["문서"]["단위"] == "㎡" + + +def test_화면_층_단추_최상위_폴더_이름_바꾸기(): + page = _text("M02_MasterTemplete_UI_Page.ts") + lib = _text("M02_MasterTemplete_UI_Side_Library.ts") + # 720-1 — 다른 층 단추 = 물음 뒤 목록표 + assert "setPre(next), void main.open(precomboList(pre))" in page + assert "void main.open(sangeunList(sk))" in page.split("const skBar")[1].split("reselect:")[0] + # 720-2 — 층 줄 「+」 = 서고 칸 맨 위 메뉴 · 메뉴 줄 = 같은 addFolder([]) + assert page.count(" add,\n") == 2 + assert "onClick: (event) => openTreeMenu(event, menuAt([]))" in lib + assert "add.hidden = !at.canWrite;" in lib and "opt.bar(add)" in lib + assert lib.count('L("M02_TreeAddTopFolder"), () => void addFolder([])') == 1 # 폴더 줄 + assert '{ label: L("M02_TreeAddTopFolder"), run: () => void addFolder([])' in lib # 문서 줄 + tree = _text("M02_MasterTemplete_TreePanel.ts") + assert '{ label: L("M02_TreeAddTopFolder"), run: () => void addFolder([]) }' in tree + # 720-3 — 문서 줄 [이름 바꾸기] · 저장 뒤 머리 제목 + assert "run: () => void renameDoc(row)" in lib + assert "이름: name }" in lib and "saveTemplate(at.layer, kind, row.이름, null" in lib + main = _text("M02_MasterTemplete_UI_Main.ts") + assert "title.textContent = at.label = named ? `${at.name} ${named}` : at.name;" in main