feat(M02): 새 구조물 조합 두 단추 — 「새 항목」 → 상세 칸 [새로 만들기] [복사 생성] · 복사 생성 = 탭 + 목록 → 지금 층 · 고른 폴더 · 새 키 · 새 번호
39-5 H4 — 나무 copyIn(붙여넣기 길 · 새 열 하나면 그 열 고름) · folder 손잡이 · ItemPick buildTwoWay(빈 카드도 씀). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
This commit is contained in:
@@ -13,3 +13,23 @@
|
|||||||
.ui-modal.m02-itempick__dialog {
|
.ui-modal.m02-itempick__dialog {
|
||||||
width: min(960px, 94vw);
|
width: min(960px, 94vw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 두 단추 화면 — 새 구조물 조합 · 빈 카드 */
|
||||||
|
.m02-itempick__two {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-12);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m02-itempick__note {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--text-body-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.m02-itempick__buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--spacing-8);
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* 다른 사람 = 사람 고르기 하나(`fetchOwners`) · 실무문서 = 마스터 일위대가 조합 줄(일위대가 고를 때만).
|
* 다른 사람 = 사람 고르기 하나(`fetchOwners`) · 실무문서 = 마스터 일위대가 조합 줄(일위대가 고를 때만).
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import { createSelectField, el, showConfirmDialog } from "@ui/ui_template_elements";
|
import { createButton, createSelectField, el, showConfirmDialog } from "@ui/ui_template_elements";
|
||||||
import { openModal } from "@ui/ui_template_modal";
|
import { openModal } from "@ui/ui_template_modal";
|
||||||
import type { SessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
import type { SessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
||||||
import { fetchOwners, type Layer } from "./M02_MasterTemplete_Api_Fetch";
|
import { fetchOwners, type Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||||
@@ -30,6 +30,8 @@ const TEXT = {
|
|||||||
person: "사람",
|
person: "사람",
|
||||||
noPeople: "다른 사람이 없음",
|
noPeople: "다른 사람이 없음",
|
||||||
failed: "목록을 못 받음 — {value}",
|
failed: "목록을 못 받음 — {value}",
|
||||||
|
makeNew: "새로 만들기",
|
||||||
|
makeCopy: "복사 생성",
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 고른 곳 — `practice` = 실무문서(마스터 일위대가 조합) */
|
/** 고른 곳 — `practice` = 실무문서(마스터 일위대가 조합) */
|
||||||
@@ -146,6 +148,54 @@ export function buildPicker(opt: PickerOptions): { root: HTMLElement } {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TwoWayOptions {
|
||||||
|
/** 위 안내 한 줄 */
|
||||||
|
note: string;
|
||||||
|
/** [새로 만들기] — 지금 길(창 · 빈 문서) */
|
||||||
|
onNew: () => void;
|
||||||
|
/** [복사 생성] 뒤 고를 목록 */
|
||||||
|
picker: Omit<PickerOptions, "onPick">;
|
||||||
|
/** 줄 누름 → 확인 글(취소면 그대로 고름) */
|
||||||
|
ask: (picked: Picked) => string;
|
||||||
|
/** 확인 뒤 — 복사(실패 알림은 부르는 쪽) */
|
||||||
|
onCopy: (picked: Picked) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** [새로 만들기] [복사 생성] 두 단추(39-5 · 답 2) — 복사 생성을 누르면 같은 칸 아래 탭 + 목록 */
|
||||||
|
export function buildTwoWay(opt: TwoWayOptions): HTMLElement {
|
||||||
|
const fresh = createButton({ label: TEXT.makeNew, variant: "filled" });
|
||||||
|
const copy = createButton({ label: TEXT.makeCopy, variant: "ghost" });
|
||||||
|
const below = el("div", { className: "m02-itempick__below" });
|
||||||
|
let busy = false;
|
||||||
|
fresh.addEventListener("click", opt.onNew);
|
||||||
|
copy.addEventListener("click", () => {
|
||||||
|
if (below.firstChild) return;
|
||||||
|
copy.classList.add("is-active");
|
||||||
|
const picker = buildPicker({
|
||||||
|
...opt.picker,
|
||||||
|
onPick: (picked) =>
|
||||||
|
void (async () => {
|
||||||
|
if (busy || !(await showConfirmDialog(opt.ask(picked)))) return;
|
||||||
|
busy = true; // 연달아 눌러도 한 번만
|
||||||
|
try {
|
||||||
|
await opt.onCopy(picked);
|
||||||
|
} finally {
|
||||||
|
busy = false;
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
});
|
||||||
|
below.append(picker.root);
|
||||||
|
});
|
||||||
|
return el("div", {
|
||||||
|
className: "m02-itempick__two",
|
||||||
|
children: [
|
||||||
|
el("p", { className: "m02-itempick__note", text: opt.note }),
|
||||||
|
el("div", { className: "m02-itempick__buttons", children: [fresh, copy] }),
|
||||||
|
below,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** 창으로 고름 — 줄 누름 → `ask` 확인(있으면) → 고른 것 · 닫으면 null */
|
/** 창으로 고름 — 줄 누름 → `ask` 확인(있으면) → 고른 것 · 닫으면 null */
|
||||||
export function pickFrom(
|
export function pickFrom(
|
||||||
opt: Omit<PickerOptions, "onPick"> & { title: string; ask?: (picked: Picked) => string },
|
opt: Omit<PickerOptions, "onPick"> & { title: string; ask?: (picked: Picked) => string },
|
||||||
|
|||||||
@@ -107,6 +107,14 @@ export interface TreePanelHandle {
|
|||||||
select: (colId: string | null) => void;
|
select: (colId: string | null) => void;
|
||||||
/** 「새 항목」 — 고른 폴더 안(없으면 맨 위)에 항목 추가 창 */
|
/** 「새 항목」 — 고른 폴더 안(없으면 맨 위)에 항목 추가 창 */
|
||||||
newItem: () => void;
|
newItem: () => void;
|
||||||
|
/** 고른 폴더(하나) — 없으면 맨 위(`[]`) · 새 항목 · 복사 생성이 들어갈 자리 */
|
||||||
|
folder: () => string[];
|
||||||
|
/** 복사 생성(39-5) — 다른 곳(`from`) 항목 키 하나를 이 층 `dest` 에 새 키로 붙이고 그 열을 고름 */
|
||||||
|
copyIn: (
|
||||||
|
from: { layer: Layer; owner: string | null },
|
||||||
|
key: string,
|
||||||
|
dest: string[],
|
||||||
|
) => Promise<void>;
|
||||||
/** 표 머리에서 온 나무 고침(이름 바꾸기 · PLAN 31-1) — 메뉴 고침과 같은 길(저장 물음 · 막힘 안내 · 다시 그림) */
|
/** 표 머리에서 온 나무 고침(이름 바꾸기 · PLAN 31-1) — 메뉴 고침과 같은 길(저장 물음 · 막힘 안내 · 다시 그림) */
|
||||||
editFromTable: (body: TreeEdit, colId: string | null) => Promise<void>;
|
editFromTable: (body: TreeEdit, colId: string | null) => Promise<void>;
|
||||||
}
|
}
|
||||||
@@ -417,8 +425,9 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle {
|
|||||||
// 새 항목은 그 열을 고름 · 지운 항목이면 고름 풀림 · 나머지는 지금 고른 열 그대로
|
// 새 항목은 그 열을 고름 · 지운 항목이면 고름 풀림 · 나머지는 지금 고른 열 그대로
|
||||||
const gone = note?.지운항목 ?? [];
|
const gone = note?.지운항목 ?? [];
|
||||||
const made = note?.열;
|
const made = note?.열;
|
||||||
const col =
|
// 붙여넣기로 새 열이 하나면(복사 생성) 그 열
|
||||||
typeof made === "string" ? made : keepCol && !gone.includes(keepCol) ? keepCol : null;
|
const one = typeof made === "string" ? made : made?.length === 1 ? made[0] : null;
|
||||||
|
const col = one ?? (keepCol && !gone.includes(keepCol) ? keepCol : null);
|
||||||
activeCol = col;
|
activeCol = col;
|
||||||
const parent = note?.경로 ? note.경로.slice(0, -1) : [];
|
const parent = note?.경로 ? note.경로.slice(0, -1) : [];
|
||||||
parent.forEach((_, i) => open.add(folderIdOf(parent.slice(0, i + 1))));
|
parent.forEach((_, i) => open.add(folderIdOf(parent.slice(0, i + 1))));
|
||||||
@@ -643,13 +652,36 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/** 상세 제목 줄 「새 항목」(30-5) — 나무 「항목 추가」와 같은 창 · 고른 폴더(하나)가 있으면 그 안 · 아니면 맨 위 */
|
/** 상세 제목 줄 「새 항목」(30-5) — 나무 「항목 추가」와 같은 창 · 고른 폴더(하나)가 있으면 그 안 · 아니면 맨 위 */
|
||||||
const newItem = (): void => {
|
const folder = (): string[] => {
|
||||||
const at = anchor && picked.has(anchor) ? targets.get(anchor) : undefined;
|
const at = anchor && picked.has(anchor) ? targets.get(anchor) : undefined;
|
||||||
void addItem(at?.kind === "folder" ? at.path : []);
|
return at?.kind === "folder" ? at.path : [];
|
||||||
|
};
|
||||||
|
const newItem = (): void => void addItem(folder());
|
||||||
|
|
||||||
|
const copyIn = async (
|
||||||
|
from: { layer: Layer; owner: string | null },
|
||||||
|
key: string,
|
||||||
|
dest: string[],
|
||||||
|
): Promise<void> => {
|
||||||
|
const run = api.paste;
|
||||||
|
if (!run || !opt.canEdit || !(await ready())) return;
|
||||||
|
await apply(() =>
|
||||||
|
run(opt.layer, owner, { kind: clipKind, from, refs: [{ 경로: [], 키: key }] }, dest),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const editFromTable = async (body: TreeEdit, colId: string | null): Promise<void> => {
|
const editFromTable = async (body: TreeEdit, colId: string | null): Promise<void> => {
|
||||||
if (opt.canEdit && (await ready())) await edit(body, colId);
|
if (opt.canEdit && (await ready())) await edit(body, colId);
|
||||||
};
|
};
|
||||||
return { root, search: search.root, add: tools, refresh, select, newItem, editFromTable };
|
return {
|
||||||
|
root,
|
||||||
|
search: search.root,
|
||||||
|
add: tools,
|
||||||
|
refresh,
|
||||||
|
select,
|
||||||
|
newItem,
|
||||||
|
folder,
|
||||||
|
copyIn,
|
||||||
|
editFromTable,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,8 +43,13 @@ import {
|
|||||||
import { canEdit, createMain, newItemButton, type Selection } from "./M02_MasterTemplete_UI_Main";
|
import { canEdit, createMain, newItemButton, type Selection } from "./M02_MasterTemplete_UI_Main";
|
||||||
import { buildSide, type AllKind } from "./M02_MasterTemplete_UI_Side";
|
import { buildSide, type AllKind } from "./M02_MasterTemplete_UI_Side";
|
||||||
import { buildAllItems } from "./M02_MasterTemplete_ItemList";
|
import { buildAllItems } from "./M02_MasterTemplete_ItemList";
|
||||||
|
import { buildTwoWay, pickedName } from "./M02_MasterTemplete_ItemPick";
|
||||||
import "./M02_MasterTemplete_UI_Style.css";
|
import "./M02_MasterTemplete_UI_Style.css";
|
||||||
|
|
||||||
|
const NEW_NOTE = "새 항목이 들어갈 자리 — {value} · 나무에서 폴더를 고르면 그 안";
|
||||||
|
const NEW_ASK =
|
||||||
|
"{from} 「{name}」 을(를) 복사해 「{to}」 에 새 항목으로 만듦 — 새 번호 · 원래 이름";
|
||||||
|
|
||||||
/** 「전체 항목」 화면 제목 앞마디 — 하위 이름 */
|
/** 「전체 항목」 화면 제목 앞마디 — 하위 이름 */
|
||||||
const ALL_TITLE = {
|
const ALL_TITLE = {
|
||||||
structure: "M02_KindStructure",
|
structure: "M02_KindStructure",
|
||||||
@@ -130,8 +135,8 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise<Mounte
|
|||||||
{
|
{
|
||||||
top: readOnlyBar?.root,
|
top: readOnlyBar?.root,
|
||||||
search: tree.search,
|
search: tree.search,
|
||||||
// 「새 항목」(30-5) — 제목 줄 오른쪽 끝 · 쓸 수 있는 층만 · 나무 「항목 추가」와 같은 창
|
// 「새 항목」(30-5) — 제목 줄 오른쪽 끝 · 쓸 수 있는 층만 · 상세 칸에 [새로 만들기] [복사 생성](39-5)
|
||||||
...(canEdit(layer, isAdmin) ? { newItem: newItemButton(() => tree.newItem()) } : {}),
|
...(canEdit(layer, isAdmin) ? { newItem: newItemButton(() => openNew()) } : {}),
|
||||||
onColumn: (column) => {
|
onColumn: (column) => {
|
||||||
readOnlyBar?.setColumn(column);
|
readOnlyBar?.setColumn(column);
|
||||||
tree.select(column);
|
tree.select(column);
|
||||||
@@ -145,6 +150,26 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise<Mounte
|
|||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
/** 새 구조물 조합(39-5 · 답 2) — 새로 만들기 = 나무 「항목 추가」 창 · 복사 생성 = 탭 + 목록 →
|
||||||
|
* 지금 보는 층 · 나무에서 고른 폴더(없으면 맨 위) · 원래 이름(겹치면 「(2)」) · 새 키 · 새 번호 */
|
||||||
|
const openNew = (): void => {
|
||||||
|
const where = (): string => tree.folder().join(" / ") || L("M02_TreeTop");
|
||||||
|
main.showView(
|
||||||
|
L("M02_TreeNewItem"),
|
||||||
|
buildTwoWay({
|
||||||
|
note: NEW_NOTE.replace("{value}", where()),
|
||||||
|
onNew: () => tree.newItem(),
|
||||||
|
picker: { user, company, kind: "item" },
|
||||||
|
ask: (picked) =>
|
||||||
|
NEW_ASK.replace("{name}", pickedName(picked))
|
||||||
|
.replace("{from}", picked.from.label)
|
||||||
|
.replace("{to}", where()),
|
||||||
|
onCopy: async (picked) => {
|
||||||
|
if ("row" in picked) await tree.copyIn(picked.from, picked.row.key, tree.folder());
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
// 첫 화면 · 닫은 뒤(양식 지움 · 자재 조합 상세 닫음) = 구조물집계표 — 빈 화면 없음
|
// 첫 화면 · 닫은 뒤(양식 지움 · 자재 조합 상세 닫음) = 구조물집계표 — 빈 화면 없음
|
||||||
const home: Selection = {
|
const home: Selection = {
|
||||||
layer,
|
layer,
|
||||||
|
|||||||
Reference in New Issue
Block a user