Merge remote-tracking branch 'origin/sub_laptop_4' into sub_laptop_1

This commit is contained in:
2026-10-04 17:41:29 +09:00
8 changed files with 91 additions and 35 deletions
@@ -510,9 +510,11 @@ export const pasteTree = (
export interface LayerOwner {
owner: string;
name: string;
/** 개인 = 집계표가 아직 없음(사람 나무 「비어 있음」 · 39-6) */
비어있음?: boolean;
}
/** 회사 = 시스템 관리자만 회사 목록 · 개인 = 그 회사 사람(본인 뺌 · 층 자리 있는 것만) */
/** 회사 = 시스템 관리자만 회사 목록 · 개인 = 그 회사를 맡은 사람 · 회사 사람 전부(나 맨 위) */
export const fetchOwners = (
layer: "company" | "personal",
company: string | null = null,
@@ -79,7 +79,8 @@ export interface LayerAccess {
needCompany: boolean;
}
/** 서버 `M02_Layer_Access.grant` 와 같은 규칙 — 남의 층(`owner`)은 늘 읽기만(읽을 수 있나는 서버가 가름) */
/** 서버 `M02_Layer_Access.grant` 와 같은 규칙 — 남의 층(`owner`) = 그 회사를 맡았으면 고침(39-6) ·
* 읽을 수 있나는 서버가 가름 */
export function accessFor(
layer: ManageLayer,
owner: string | null,
@@ -87,10 +88,14 @@ export function accessFor(
): LayerAccess {
if (layer === "system") return { canWrite: user?.role === "SYSTEM_ADMIN", needCompany: false };
const own = ownOwner(layer, user);
if (owner && owner !== own) return { canWrite: false, needCompany: false };
if (owner && owner !== own) {
return { canWrite: manages(owner.split("/")[0], user), needCompany: false };
}
if (!own) return { canWrite: false, needCompany: true };
const companyAdmin = user?.role === "ADMIN" || user?.role === "SYSTEM_ADMIN" || !!user?.is_master;
return { canWrite: layer === "personal" || companyAdmin, needCompany: false };
return {
canWrite: layer === "personal" || manages(ownOwner("company", user), user),
needCompany: false,
};
}
/** 회사 C 를 맡음 — 시스템 관리자 · 또는 C 의 회사 관리자(서버 `M02_Layer_Access.manages` 짝 · 39-6) */
@@ -35,6 +35,7 @@ import {
type TreeRef,
type TreeView,
} from "./M02_MasterTemplete_Api_Fetch";
import { askCopyFolder, type CopyTarget } from "./M02_MasterTemplete_TreePanel_CopyTo";
import { askFolder, askNewItem, askText } from "./M02_MasterTemplete_TreePanel_Dialogs";
import { attachTreeDrag } from "./M02_MasterTemplete_TreePanel_Drag";
import {
@@ -83,8 +84,8 @@ export interface TreePanelOptions {
};
/** 항목 추가 창의 일위대가 고르기(PLAN 31-2 · 세 층 조합 · 찾기 AND) — 없으면 그 줄 없음 */
pickCombo?: () => Promise<{ key: string; label: string } | null>;
/** 「〇〇로 복사」 — 고른 것을 그 층 맨 위에 바로(층을 안 바꿈 · `label` 의 {n} = 개수 · `api.paste` 로) */
copyTo?: { layer: Layer; owner: string | null; label: string }[];
/** 「〇〇로 복사…」 — 그 층 폴더를 골라 바로(층을 안 바꿈 · `label` 의 {n} = 개수 · `api.paste` 로 · 39-6) */
copyTo?: CopyTarget[];
/** 이 층을 고칠 수 있나 — 아니면 메뉴 · 키 없음 */
canEdit: boolean;
/** 항목을 누름 — 표에서 그 열을 고름 */
@@ -542,18 +543,20 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle {
showToast(L("M02_TreeCopied").replace("{n}", String(list.length)), "success");
}
/** 「〇〇로 복사」 — 그 층 맨 위에 바로(새 키 · 같은 이름 「(2)」 · 이 나무는 그대로) */
async function copyTo(
list: Target[],
to: { layer: Layer; owner: string | null; label: string },
): Promise<void> {
/** 「〇〇로 복사…」 — 그 층 폴더를 고름(`askCopyFolder`) → 새 키 · 같은 이름 「(2)」 · 이 나무는 그대로 */
async function copyTo(list: Target[], to: CopyTarget): Promise<void> {
if (!api.paste || !list.length) return;
const clip = { kind: clipKind, from: { layer: opt.layer, owner }, refs: list.map(refOf) };
try {
await api.paste(to.layer, to.owner, clip, []);
const where = to.label.replace(/ ?\(.*$/, "").replace(/(으로|로) 복사$/, "");
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),
L("M02_TreeCopiedTo")
.replace("{n}", String(list.length))
.replace("{layer}", where)
.replace("{folder}", dest.join(" / ") || L("M02_TreeTop")),
"success",
);
} catch (error) {
@@ -0,0 +1,26 @@
/* =============================================================================
* M02_MasterTemplete_TreePanel_CopyTo.ts
* 「〇〇로 복사…」(39-6) — 붙일 층의 나무를 읽어 대상 폴더를 고름(맨 위 · 폴더) → 경로 · 취소 = null.
* 아직 만든 것이 없는 남의 층(404)은 맨 위만 — 첫 쓰기 때 서버가 소속을 보고 뼈대를 세움.
* ========================================================================== */
import type { Layer, TreeView } from "./M02_MasterTemplete_Api_Fetch";
import { askFolder } from "./M02_MasterTemplete_TreePanel_Dialogs";
import { buildFolders, listFolders } from "./M02_MasterTemplete_TreePanel_Model";
export interface CopyTarget {
layer: Layer;
owner: string | null;
/** 메뉴 글 — `{n}` = 개수 */
label: string;
}
export async function askCopyFolder(
fetchTree: (layer: Layer, owner: string | null) => Promise<TreeView>,
to: CopyTarget,
n: number,
): Promise<string[] | null> {
const view = await fetchTree(to.layer, to.owner).catch(() => null);
const folders = view ? listFolders(buildFolders(view)) : [];
return askFolder(to.label.replace("{n}", String(n)), folders, () => false, []);
}
@@ -4,8 +4,8 @@
* 「구조물 조합」 아래 한 줄 · 나무 종류와 무관한 공용 부품. 고른 층의 나무는 부르는 쪽이 이 줄 아래에 붙임.
* 권한 없는 층 = 단추 글 끝에 「읽기」(권한 = `accessFor` · 서버 `M02_Layer_Access` 와 짝)
* 회사 = 볼 회사(`viewCompany` — 시스템 관리자는 해시 `company` · `aislo.adminCompany` · 자기 회사)
* 시스템 관리자가 남의 회사를 보면 그 회사 관리자처럼(PLAN 29-6) — 「개인」 = 그 회사 사람 나무
* (`createPeopleTree` · 사람 줄 → 그 사람 라이브러리 · 읽기) · 자기 개인 층은 자기 회사를 고를 때만
* 볼 회사를 맡으면(`manages` · 39-6 — 시스템 관리자 · 그 회사 관리자) 「개인」 = 그 회사 사람 나무
* (`createPeopleTree` · 나 맨 위 · 사람 줄 → 그 사람 라이브러리 · 고침) · 일반 = 내 개인 그대로
* 누르면 `go`(부르는 쪽이 해시로 다시 엶).
* ========================================================================== */
@@ -18,6 +18,7 @@ import {
accessFor,
buildNeedCompany,
layerOfRoot,
manages,
ownOwner,
type Root,
} from "./M02_MasterTemplete_Layer";
@@ -40,9 +41,9 @@ export interface LayerBarOptions {
export const companyOwner = (company: string | null, user: SessionUser | null): string | null =>
company && company !== ownOwner("company", user) ? company : null;
/** 시스템 관리자가 남의 회사를 봄 — 「개인」 단추 = 그 회사 사람 나무(읽기) */
/** 볼 회사를 맡음(39-6) — 「개인」 단추 = 그 회사 사람 나무 */
export const seesOthers = (company: string | null, user: SessionUser | null): boolean =>
user?.role === "SYSTEM_ADMIN" && !!companyOwner(company, user);
manages(company, user);
export function createLayerBar(opt: LayerBarOptions): HTMLElement {
const { user, active } = opt;
@@ -57,7 +58,7 @@ export function createLayerBar(opt: LayerBarOptions): HTMLElement {
}
function button(root: Root, owner: string | null, people = false): HTMLButtonElement {
const readOnly = people || !accessFor(layerOfRoot(root), owner, user).canWrite;
const readOnly = !people && !accessFor(layerOfRoot(root), owner, user).canWrite;
const name = L(
root === "master"
? "M02_PickerSystem"
@@ -113,7 +114,8 @@ export function createPeopleTree(opt: PeopleTreeOptions): HTMLElement {
box.replaceChildren(
...rows.flatMap((one) => {
const current = one.owner === opt.active;
const node = { id: one.owner, label: one.name, count: null, folder: true };
const label = one.비어있음 ? `${one.name} · ${L("M02_PeopleEmpty")}` : one.name;
const node = { id: one.owner, label, count: null, folder: true };
const row = createTreeRow(node, current ? "▾" : "▸", 0, () => {
if (!current) return opt.go(one.owner);
wrap.hidden = !wrap.hidden;
@@ -81,12 +81,15 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise<Mounte
// 자기 회사용을 읽기만 볼 때 — 안내 + [개인용으로 복사]
const readOnlyBar = layer === "company" && !owner && !isAdmin ? buildReadOnlyBar(user) : null;
// 고른 층의 나무 — 고르기 모달과 같은 폴더 나무 · 좌측 「구조물 조합」 줄 아래
// 「〇〇로 복사」(30-2) — 이 사람이 쓸 수 있는 자기 층(지금 보는 층은 뺌) 맨 위로 바로
// 「〇〇로 복사…」(30-2 · 39-6) — 쓸 수 있는 층 전부(마스터 · 볼 회사 · 내 개인 · 지금 보는 층은 뺌) ·
// 대상 폴더는 고르는 창
const here = owner === ownOwner(layer, user) ? null : owner;
const copyTo = (["master", "company", "personal"] as Root[]).flatMap((one) => {
const to = layerOfRoot(one);
if (!accessFor(to, null, user).canWrite || (to === layer && !owner)) return [];
const toOwner = one === "company" ? companyOwner(company, user) : null;
if (!accessFor(to, toOwner, user).canWrite || (to === layer && toOwner === here)) return [];
const key = one === "master" ? "Master" : one === "company" ? "Company" : "Personal";
return [{ layer: to, owner: null, label: L(`M02_TreeCopyTo${key}`) }];
return [{ layer: to, owner: toOwner, label: L(`M02_TreeCopyTo${key}`) }];
});
const tree = createTreePanel({
layer,