From 4fa49e307afb182fa0a722888a21795ec44b1019 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 30 Sep 2026 19:22:02 +0900 Subject: [PATCH] =?UTF-8?q?feat(M02):=20=EC=9D=BC=EC=9C=84=EB=8C=80?= =?UTF-8?q?=EA=B0=80=20=EB=82=98=EB=AC=B4=EC=97=90=20=EC=97=AC=EB=9F=AC=20?= =?UTF-8?q?=EA=B0=9C=20=EA=B3=A0=EB=A5=B4=EA=B8=B0=20=C2=B7=20=EB=B3=B5?= =?UTF-8?q?=EC=82=AC=20=C2=B7=20=EB=B6=99=EC=97=AC=EB=84=A3=EA=B8=B0=20?= =?UTF-8?q?=C2=B7=20=E3=80=87=E3=80=87=EB=A1=9C=20=EB=B3=B5=EC=82=AC=20?= =?UTF-8?q?=C2=B7=20=EC=97=AC=EB=9F=AC=20=EA=B0=9C=20=EC=98=AE=EA=B8=B0?= =?UTF-8?q?=EA=B8=B0=20=C2=B7=20=EC=A7=80=EC=9A=B0=EA=B8=B0=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0(PLAN=2030-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Labor 나무 api.many → /combos/tree-many · api.paste → /combos/paste(대상 층 = 부품이 준 층 · 다른 층이면 이 나무 그대로) · clipKind 「일위대가」 - 「〇〇로 복사」 목록은 페이지가 구조물 나무와 같은 것을 줌(UI_Page 한 줄) - 시험 test_m02_labor_tree 한 건 더함 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BU9gbNE3aWFUvE61pS2WGt --- .../M02_MasterTemplete_Labor.ts | 33 +++++++++++++++++++ .../M02_MasterTemplete_UI_Page.ts | 3 +- resources/tester/test_m02_labor_tree.py | 11 +++++++ 3 files changed, 46 insertions(+), 1 deletion(-) 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 # 구조물 나무와 같은 「〇〇로 복사」 목록