diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor.ts index b4df0763..6696c345 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor.ts @@ -11,7 +11,9 @@ import { ApiError } from "../M01_MasterData/M01_MasterData_UI_Logic_Api"; import { StaleError } from "./M02_MasterTemplete_Api_Fetch"; import { editComboTree, + editComboTreeMany, fetchComboList, + pasteCombos, type ComboList, type ComboTreeEdit, type LaborScope, @@ -33,6 +35,8 @@ function treeError(error: unknown): Error { export interface LaborLibraryOptions { scope: LaborScope; canEdit: boolean; + /** 「〇〇로 복사」 — 이 사람이 쓸 수 있는 층(지금 보는 층 뺌 · 페이지가 구조물 나무와 같은 목록을 줌) */ + copyTo?: { layer: LaborScope["layer"]; owner: string | null; label: string }[]; /** 나무 항목을 누름 — 메인 칸에 일위대가 화면을 먼저 띄움 */ reveal: () => Promise; } @@ -86,7 +90,36 @@ export function buildLaborLibrary(opt: LaborLibraryOptions): LaborLibrary { }); return toView(took(got), noteOf(edit, got.지운조합)); }, + // 여러 개 옮기기 · 지우기(PLAN 30-3) — 고른 것마다 나무 고침 하나 · 서버가 한 번에 저장 + many: async (_layer, body, 판) => { + const ops = body.대상.map((ref) => ({ + op: body.op, + ...(ref.키 ? { 키: ref.키 } : { 경로: ref.경로 }), + ...(body.op === "move" ? { 새경로: body.새경로 ?? [] } : {}), + })); + const got = await editComboTreeMany(ops, 판, scope).catch((error: unknown) => { + throw treeError(error); + }); + return toView(took(got), { op: body.op, 지운항목: got.지운조합 }); + }, + // 붙여넣기 · 「〇〇로 복사」 — 대상 층(`layer` · `owner`)에 새 키 · 이 나무가 아니면 이 나무는 그대로 + paste: async (layer, owner, clip, dest) => { + const to = { layer, owner } as LaborScope; + const pick = { + 키: clip.refs.flatMap((ref) => (ref.키 ? [ref.키] : [])), + 폴더: clip.refs.flatMap((ref) => (ref.키 ? [] : [ref.경로])), + }; + const got = await fetchComboList(to) + .then((list) => pasteCombos(clip.from as LaborScope, pick, dest, list.판, to)) + .catch((error: unknown) => { + throw treeError(error); + }); + const here = layer === scope.layer && (owner ?? null) === (scope.owner ?? null); + return toView(took(here ? got : await fetchComboList(scope))); + }, }, + clipKind: "일위대가", + copyTo: opt.copyTo, onPick: async (item) => { await opt.reveal(); tree.select(item.key); diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts index 2bc95b0c..7e3941ed 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts @@ -163,7 +163,8 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise layerBar("일위대가", add, back), wrap: (panel) => peopleTree("일위대가", panel), - make: (reveal) => buildLaborLibrary({ scope: { layer, owner }, canEdit: isAdmin, reveal }), + make: (reveal) => + buildLaborLibrary({ scope: { layer, owner }, canEdit: isAdmin, reveal, copyTo }), }), }); diff --git a/resources/tester/test_m02_labor_tree.py b/resources/tester/test_m02_labor_tree.py index dec0d577..de58350b 100644 --- a/resources/tester/test_m02_labor_tree.py +++ b/resources/tester/test_m02_labor_tree.py @@ -90,3 +90,14 @@ def test_뿌리를_바꿔도_일위대가_화면_유지_해시도_맞춤(): assert 'mark("일위대가")' in labor # 여는 순간 해시 open assert "history.replaceState" in page # hashchange 없이(화면 다시 안 그림) assert 'if (main.current() && view === "일위대가") mark("구조물조합")' in page + + +def test_일위대가_나무에_여러_개_복사_붙여넣기_연결(): + """PLAN 30-3 — 공용 나무 부품(30-2) 콜백을 일위대가 서버 길에 이음 · + 붙임 종류는 따로(구조물과 안 섞임).""" + labor = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Labor.ts").read_text("utf-8") + assert 'clipKind: "일위대가"' in labor and "copyTo: opt.copyTo" in labor + assert "editComboTreeMany(ops, 판, scope)" in labor # 여러 개 옮기기 · 지우기 + assert "pasteCombos(clip.from as LaborScope, pick, dest, list.판, to)" in labor # 대상 층 + page = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts").read_text("utf-8") + assert "canEdit: isAdmin, reveal, copyTo })" in page # 구조물 나무와 같은 「〇〇로 복사」 목록