From 194e1a9c39d9cf2b8c891f261f23e13e494df83c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 1 Nov 2026 21:21:29 +0900 Subject: [PATCH] =?UTF-8?q?feat(720-2):=20=EC=B5=9C=EC=83=81=EC=9C=84=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=EB=A7=8C=EB=93=A4=EA=B8=B0=20=E2=80=94=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=C2=B7=20=EB=AC=B8=EC=84=9C=20=EC=A4=84=20?= =?UTF-8?q?=EB=A9=94=EB=89=B4=20+=20=EB=8B=A8=EA=B0=80=EC=82=B0=EC=B6=9C?= =?UTF-8?q?=20=C2=B7=20=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20=EC=B8=B5=20?= =?UTF-8?q?=EC=A4=84=20=E3=80=8C+=E3=80=8D(=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=95=A9=EA=B3=BC=20=EA=B0=99=EC=9D=80=20addFolder([]?= =?UTF-8?q?))?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 구조물 조합 나무 「〇〇로 복사…」 몸을 TreePanel_CopyTo 로 뗌(TreePanel 700 → 688 줄 · 동작 같음) - 낡은 시험 test_m02_layer_screen(복사 대상 글 자리 = CopyTo) · test_m02_precombo_scope(층 줄 add 인자) 새 꼴로 Co-Authored-By: Claude Opus 5.5 --- .../M02_MasterTemplete_TreePanel.ts | 28 ++++--------- .../M02_MasterTemplete_TreePanel_CopyTo.ts | 39 ++++++++++++++++++- .../M02_MasterTemplete_UI_Page.ts | 7 +++- .../M02_MasterTemplete_UI_Side_Library.ts | 24 +++++++++--- .../M02_MasterTemplete_UI_Side_Precombo.ts | 5 ++- resources/tester/test_m02_layer_screen.py | 5 ++- resources/tester/test_m02_precombo_scope.py | 6 +-- ui_template/ui_template_locale_m2.ts | 1 + 8 files changed, 80 insertions(+), 35 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts index f8fa81004..7a9050f46 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts @@ -36,7 +36,7 @@ import { type TreeRef, type TreeView, } from "./M02_MasterTemplete_Api_Fetch"; -import { askCopyFolder, type CopyTarget } from "./M02_MasterTemplete_TreePanel_CopyTo"; +import { copyToLayer, type CopyTarget } from "./M02_MasterTemplete_TreePanel_CopyTo"; import { askFolder, askNewItem, askText } from "./M02_MasterTemplete_TreePanel_Dialogs"; import { attachTreeDrag } from "./M02_MasterTemplete_TreePanel_Drag"; import { @@ -196,7 +196,7 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { { label: L("M02_TreeCopy").replace("{n}", n), run: () => copy(list) }, ...(opt.copyTo ?? []).map((to) => ({ label: to.label.replace("{n}", n), - run: () => void copyTo(list, to), + run: () => copyTo(list, to), })), ]; }; @@ -243,6 +243,7 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { ...(isTop ? [] : [ + { label: L("M02_TreeAddTopFolder"), run: () => void addFolder([]) }, // 720-2 = 「+」 길 { label: L("M02_TreeRename"), run: () => void rename(target) }, { label: L("M02_TreeMove"), run: () => void move(target) }, { label: L("M02_Delete"), run: () => void remove(target) }, @@ -554,25 +555,12 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { showToast(L("M02_TreeCopied").replace("{n}", String(list.length)), "success"); } - /** 「〇〇로 복사…」 — 그 층 폴더를 고름(`askCopyFolder`) → 새 키 · 같은 이름 「(2)」 · 이 나무는 그대로 */ - async function copyTo(list: Target[], to: CopyTarget): Promise { - if (!api.paste || !list.length) return; + /** 「〇〇로 복사…」(39-6) — 몸은 `TreePanel_CopyTo` */ + function copyTo(list: Target[], to: CopyTarget): void { + const paste_ = api.paste; + if (!paste_ || !list.length) return; const clip = { kind: clipKind, from: { layer: opt.layer, owner }, refs: list.map(refOf) }; - try { - const dest = await askCopyFolder(api.fetch, to, list.length); - if (!dest) return; - await api.paste(to.layer, to.owner, clip, dest); - const where = to.label.replace(/ ?\(.*$/, "").replace(/(으로|로) 복사…?$/, ""); - showToast( - L("M02_TreeCopiedTo") - .replace("{n}", String(list.length)) - .replace("{layer}", where) - .replace("{folder}", dest.join(" / ") || L("M02_TreeTop")), - "success", - ); - } catch (error) { - showToast(L("M02_ActionFailed").replace("{value}", why(error)), "error"); - } + void copyToLayer({ fetch: api.fetch, paste: paste_ }, clip, to); } async function paste(dest: string[]): Promise { diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel_CopyTo.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel_CopyTo.ts index ae6ab3e8b..c36404fee 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel_CopyTo.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel_CopyTo.ts @@ -2,10 +2,12 @@ * M02_MasterTemplete_TreePanel_CopyTo.ts * 「〇〇로 복사…」(39-6) — 붙일 층의 나무를 읽어 대상 폴더를 고름(맨 위 · 폴더) → 경로 · 취소 = null. * 아직 만든 것이 없는 남의 층(404)은 맨 위만 — 첫 쓰기 때 서버가 소속을 보고 뼈대를 세움. + * `copyToLayer` = 고름 → 붙여넣기 → 알림(구조물 조합 나무 `TreePanel` 에서 뗌 · 720-2). * ========================================================================== */ +import { showToast } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; -import type { Layer, TreeView } from "./M02_MasterTemplete_Api_Fetch"; +import type { Layer, TreeClip, TreeView } from "./M02_MasterTemplete_Api_Fetch"; import { askFolder } from "./M02_MasterTemplete_TreePanel_Dialogs"; import { buildFolders, listFolders } from "./M02_MasterTemplete_TreePanel_Model"; @@ -26,3 +28,38 @@ export async function askCopyFolder( const title = to.label.replace("{n}", String(n)); return askFolder(title, folders, () => false, [], L("M02_TreeCopyFolder")); } + +/** 「〇〇로 복사…」 — 그 층 폴더를 고름(`askCopyFolder`) → 새 키 · 같은 이름 「(2)」 · 부른 나무는 그대로 */ +export async function copyToLayer( + api: { + fetch: (layer: Layer, owner: string | null) => Promise; + paste: ( + layer: Layer, + owner: string | null, + clip: TreeClip, + dest: string[], + ) => Promise; + }, + clip: TreeClip, + to: CopyTarget, +): Promise { + const n = String(clip.refs.length); + try { + const dest = await askCopyFolder(api.fetch, to, clip.refs.length); + if (!dest) return; + await api.paste(to.layer, to.owner, clip, dest); + const where = to.label.replace(/ ?\(.*$/, "").replace(/(으로|로) 복사…?$/, ""); + showToast( + L("M02_TreeCopiedTo") + .replace("{n}", n) + .replace("{layer}", where) + .replace("{folder}", dest.join(" / ") || L("M02_TreeTop")), + "success", + ); + } catch (error) { + showToast( + L("M02_ActionFailed").replace("{value}", error instanceof Error ? error.message : ""), + "error", + ); + } +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts index 395bb2e8d..d56440cb8 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts @@ -239,11 +239,13 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise + // 층 단추 옆 「+」(720-2) = 서고 칸이 만든 맨 위 메뉴 + const preBar = (add: HTMLElement): HTMLElement => createLayerBar({ user, active: { root: pre.root, owner: pre.owner }, company, + add, // 다른 층 = 저장 안 한 고침 물음 → 그 층 나무 + 목록표(720-1) · 켜진 단추 다시 누름 = 목록표 go: (next) => void main @@ -254,11 +256,12 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise + const skBar = (add: HTMLElement): HTMLElement => createLayerBar({ user, active: { root: sk.root, owner: sk.owner }, company, + add, go: (next) => void main .confirmLeave() diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Library.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Library.ts index a2180e8f8..483834e96 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Library.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Library.ts @@ -8,10 +8,11 @@ * 여럿(구조물 조합 나무 꼴 · `TreePanel_Pick`) — Ctrl · Shift 클릭 고름 · 오른쪽 클릭 · Ctrl+C · Ctrl+V · * Delete 가 고른 것 전부에 · 복사는 읽을 수 있는 층 어디서나 → 쓸 수 있는 층에 붙여넣기(새 키). * 문서 누름 = 편집 칸 · 켜진 층 단추 다시 누름 = 목록표(페이지 몫). + * 최상위 폴더(720-2) — 층 단추 옆 「+」(맨 위 메뉴) · 폴더 · 문서 줄 [최상위 폴더 만들기] = 같은 `addFolder([])`. * ========================================================================== */ import { isCtrlKey } from "../A00_Common/key_shortcut"; -import { el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; +import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; import { createEmpty } from "@ui/ui_template_empty"; import { t as L } from "@ui/ui_template_locale"; import { @@ -58,8 +59,8 @@ export interface LibraryScope { export interface LibraryOptions { kind: LibraryKind; scope: () => LibraryScope; - /** 층 단추 줄(페이지가 만듦 · 그릴 때마다 새로 — 켜진 단추가 바뀜) */ - bar: () => HTMLElement; + /** 층 단추 줄(페이지가 만듦 · 그릴 때마다 새로 — 켜진 단추가 바뀜) · `add` = 단추 옆 「+」(720-2) */ + bar: (add: HTMLElement) => HTMLElement; confirmLeave: () => Promise; onOpen: (sel: Selection | null) => void; getOpen: () => Selection | null; @@ -130,6 +131,15 @@ export function buildLibrarySection(opt: LibraryOptions): LibraryHandle { className: "ui-tree m02-tree__body", attrs: { "data-library-tree": kind, tabindex: "0" }, }); + // 층 단추 옆 「+」(720-2 · 구조물 조합 꼴) — 맨 위 메뉴 · 읽기만 층 = 숨김(그릴 때) + const add = createButton({ + label: "+", + variant: "ghost", + onClick: (event) => openTreeMenu(event, menuAt([])), + }); + add.classList.add("m02-tree__add"); + add.title = L("M02_TreeAddTop"); + add.setAttribute("aria-label", L("M02_TreeAddTop")); const leaves = new Map(); const toggled = new Map(); // 손으로 펴고 접은 폴더 — 다시 그려도 그대로 const targets = new Map(); @@ -255,6 +265,7 @@ export function buildLibrarySection(opt: LibraryOptions): LibraryHandle { item(opt.text.newHere, () => make(path)), ...(path.length ? [ + item(L("M02_TreeAddTopFolder"), () => void addFolder([])), item(L("M02_TreeRename"), () => void rename(path)), item(L("M02_Delete"), () => void remove([target])), { label: L("M02_TreeCopy").replace("{n}", "1"), run: () => copy([target]) }, @@ -288,12 +299,14 @@ export function buildLibrarySection(opt: LibraryOptions): LibraryHandle { const item = { key: row.이름, 열: row.이름, 경로: [...folder, label], 이름: label }; const target: Target = { kind: "item", item }; targets.set(id, target); + 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_TreeCopy").replace("{n}", "1"), run: () => copy([target]) }, { label: L("M02_Delete"), run: () => void remove([target]), - disabledReason: opt.scope().canWrite ? undefined : tc("ReadOnly"), + disabledReason: off(), }, ...(opt.more?.(row.이름, label) ?? []), ]; @@ -424,7 +437,8 @@ export function buildLibrarySection(opt: LibraryOptions): LibraryHandle { const refresh = async (keep = false): Promise => { const mine = ++seq; const at = opt.scope(); - barHost.replaceChildren(opt.bar()); + add.hidden = !at.canWrite; + barHost.replaceChildren(opt.bar(add)); if (!keep) treeHost.replaceChildren(createEmpty({ text: tc("Loading"), small: true })); const [got, tree] = await Promise.all([ // 못 받으면 빈 나무(목록표에서 까닭) · 폴더를 못 받으면 문서는 모두 뿌리 diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Precombo.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Precombo.ts index b8a79ae16..c97bf1697 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Precombo.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Precombo.ts @@ -15,6 +15,7 @@ import type { Selection } from "./M02_MasterTemplete_UI_Main"; import { buildLibrarySection, type LibraryHandle, + type LibraryOptions, type LibraryScope, } from "./M02_MasterTemplete_UI_Side_Library"; @@ -53,8 +54,8 @@ export const precomboList = (scope: PrecomboScope): Selection => ({ export interface PrecomboOptions { scope: () => PrecomboScope; - /** 층 단추 줄(페이지가 만듦 · 그릴 때마다 새로 — 켜진 단추가 바뀜) */ - bar: () => HTMLElement; + /** 층 단추 줄(페이지가 만듦 · 그릴 때마다 새로 — 켜진 단추가 바뀜) · `add` = 「+」(720-2) */ + bar: LibraryOptions["bar"]; confirmLeave: () => Promise; onOpen: (sel: Selection | null) => void; getOpen: () => Selection | null; diff --git a/resources/tester/test_m02_layer_screen.py b/resources/tester/test_m02_layer_screen.py index a74df5040..8e58b5371 100644 --- a/resources/tester/test_m02_layer_screen.py +++ b/resources/tester/test_m02_layer_screen.py @@ -296,9 +296,10 @@ def test_켜진_층_단추는_목록으로_복사_끌기(): assert "(to === layer && toOwner === here)) return [];" in page copy_to = _text(M02 / "M02_MasterTemplete_TreePanel_CopyTo.ts") assert "listFolders(buildFolders(view))" in copy_to and ".catch(() => null)" in copy_to - assert "const dest = await askCopyFolder(api.fetch, to, list.length);" in _text( + assert "const dest = await askCopyFolder(api.fetch, to, clip.refs.length);" in copy_to + assert "void copyToLayer({ fetch: api.fetch, paste: paste_ }, clip, to);" in _text( M02 / "M02_MasterTemplete_TreePanel.ts" - ) + ) # 720-2 — 몸은 CopyTo 로 뗌(TreePanel 700 줄) panel = _text(M02 / "M02_MasterTemplete_TreePanel.ts") assert "if (opt.canEdit) drag.arm(button, one.id);" in panel drag = _text(M02 / "M02_MasterTemplete_TreePanel_Drag.ts") diff --git a/resources/tester/test_m02_precombo_scope.py b/resources/tester/test_m02_precombo_scope.py index 1ae74c681..660859a49 100644 --- a/resources/tester/test_m02_precombo_scope.py +++ b/resources/tester/test_m02_precombo_scope.py @@ -72,12 +72,12 @@ def test_사전조합_층_단추는_페이지를_다시_안_세움(): assert "history.replaceState(" in pre and "side.refreshPrecombo()" in pre assert "navigateTo" not in pre and "setActiveLayer" not in pre # 페이지 층 · 해시 이동 없음 assert "entry.pre = pre.root;" in pre # 구조물 조합 층 이동(go) 때도 주소에 남음 - bar = page.split("const preBar = (): HTMLElement =>")[1].split("const side = buildSide(")[0] + bar = page.split("const preBar = (add: HTMLElement): HTMLElement =>")[1].split("const side = buildSide(")[0] # 720-1 — 다른 층 = 저장 안 한 고침 물음 → 그 층 나무 + 목록표 go = bar.split("go: (next) =>")[1].split("reselect:")[0] - assert "main.confirmLeave()" in go and "setPre(next), void main.open(precomboList(pre))" in go + assert ".confirmLeave()" in go and "setPre(next), void main.open(precomboList(pre))" in go sk = page.split("const skBar = (")[1].split("const side = buildSide(")[0] - assert "main.confirmLeave()" in sk and "void main.open(sangeunList(sk))" in sk.split("reselect:")[0] + assert ".confirmLeave()" in sk and "void main.open(sangeunList(sk))" in sk.split("reselect:")[0] # 구조물 조합 층 단추는 그대로 해시 이동 한 벌 assert page.count('layerBar("') == 1 assert 'layerBar("구조물조합", tree.add, () => void main.openTable(layer)),' in page diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index cd9687885..7a008cc83 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -107,6 +107,7 @@ export const ui_locales_m2 = { M02_TreeAddTop: ["맨 위에 폴더 · 항목 추가", "Add folder or item at top"], M02_TreeEmpty: ["항목이 없음", "No items"], M02_TreeAddFolder: ["폴더 추가", "Add folder"], + M02_TreeAddTopFolder: ["최상위 폴더 만들기", "New top-level folder"], M02_TreeAddItem: ["항목 추가", "Add item"], M02_TreeRename: ["이름 바꾸기", "Rename"], M02_TreeMove: ["옮기기", "Move"],