- 층 권한 한 벌 M02_Layer_Access — 나무 · 항목(Router_Items) · 양식(Router_Layers) 같은 규칙 · owner 인자 · 남의 층은 읽기만 · 읽기로 남의 층에 뼈대 안 씀
- 남의 층 목록 GET /api/m02/layers/{company|personal}/owners — 시스템 관리자 회사 목록 · 회사 관리자 같은 회사 사람
- ?layer= 페이지 분리 폐기 → 한 화면 뿌리 셋(마스터 · 회사 · 개인) · 뿌리 부품 TreePanel_Roots(펼칠 때 불러옴) · TreePanel 은 owner · api 를 받는 공용 부품
- 진입 해시 ?open=구조물조합|일위대가|옵션|도면&root=&owner= · 항목 고르기 [관리 화면에서 고치기]도 이 해시
- 「항목 나무」 → 「구조물 라이브러리」
- 시험 test_m02_layer_access 새로 · layer_screen · template_layers 새 규칙으로
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
128 lines
5.4 KiB
TypeScript
128 lines
5.4 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_Layer.ts
|
|
* M02 한 화면의 뿌리 셋(마스터 · 회사 · 개인 · PLAN 27-1) — 진입 해시
|
|
* `#/m02-master-template?open=구조물조합|일위대가|옵션|도면&root=master|company|personal&owner=…`
|
|
* (옛 `?layer=` 도 root 로 읽음) · 고칠 권한(`accessFor` — 서버 `M02_Layer_Access` 와 짝 · 27-1c) ·
|
|
* 회사 없음 안내 · 회사용 읽기만일 때 [개인용으로 복사] 막대.
|
|
* ========================================================================== */
|
|
|
|
import { ROUTES } from "@config/config_frontend";
|
|
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
|
import { t as L } from "@ui/ui_template_locale";
|
|
import { currentQuery, navigateTo } from "../A00_Common/router";
|
|
import type { SessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
|
import { copyItemToPersonal, fetchStructureCombos } from "./M02_MasterTemplete_Api_Fetch";
|
|
|
|
export type ManageLayer = "system" | "company" | "personal";
|
|
/** 해시 · 화면의 뿌리 이름 — 서버 층 이름은 `layerOfRoot` */
|
|
export type Root = "master" | "company" | "personal";
|
|
export type OpenTarget = "구조물조합" | "일위대가" | "옵션" | "도면";
|
|
|
|
export interface Entry {
|
|
root: Root;
|
|
/** 남의 층 주인(회사 `{C}` · 개인 `{C}/{U}`) — 자기 층 = null */
|
|
owner: string | null;
|
|
open: OpenTarget;
|
|
}
|
|
|
|
const OPENS: OpenTarget[] = ["구조물조합", "일위대가", "옵션", "도면"];
|
|
|
|
export const layerOfRoot = (root: Root): ManageLayer => (root === "master" ? "system" : root);
|
|
|
|
/** 주소 인자에서 진입 — 모르는 값 · 없음 = 마스터 · 구조물 조합 */
|
|
export function entryFromHash(): Entry {
|
|
const query = currentQuery();
|
|
const raw = query.get("root") ?? query.get("layer");
|
|
const root: Root = raw === "company" || raw === "personal" ? raw : "master";
|
|
const open = OPENS.find((one) => one === query.get("open")) ?? "구조물조합";
|
|
return { root, owner: root === "master" ? null : query.get("owner") || null, open };
|
|
}
|
|
|
|
/** 진입 해시 인자 — `navigateTo(ROUTES.M02_MASTER_TEMPLATE, entryQuery(…))` */
|
|
export const entryQuery = (entry: Entry): Record<string, string> => ({
|
|
open: entry.open,
|
|
root: entry.root,
|
|
...(entry.owner ? { owner: entry.owner } : {}),
|
|
});
|
|
|
|
/** 자기 층 주인 글 — 회사 없으면 null */
|
|
export const ownOwner = (layer: ManageLayer, user: SessionUser | null): string | null =>
|
|
!user?.company_id || layer === "system"
|
|
? null
|
|
: layer === "company"
|
|
? String(user.company_id)
|
|
: `${user.company_id}/${user.id}`;
|
|
|
|
export interface LayerAccess {
|
|
/** 이 층을 고칠 수 있나 — 서버도 같은 규칙으로 막음 */
|
|
canWrite: boolean;
|
|
/** 회사 · 개인 자기 층인데 회사가 없음 — 「회사부터 등록」 안내 */
|
|
needCompany: boolean;
|
|
}
|
|
|
|
/** 서버 `M02_Layer_Access.grant` 와 같은 규칙 — 남의 층(`owner`)은 늘 읽기만(읽을 수 있나는 서버가 가름) */
|
|
export function accessFor(
|
|
layer: ManageLayer,
|
|
owner: string | null,
|
|
user: SessionUser | null,
|
|
): 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 (!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 };
|
|
}
|
|
|
|
/** 회사 없음 — 개인 · 회사 층은 회사 폴더 아래라 회사부터 */
|
|
export function buildNeedCompany(): HTMLElement {
|
|
const go = createButton({ label: L("M02_NeedCompanyGo"), variant: "filled" });
|
|
go.addEventListener("click", () => navigateTo(ROUTES.B01_ACCOUNT));
|
|
return el("div", {
|
|
className: "m02-layer-need",
|
|
children: [el("p", { text: L("M02_NeedCompany") }), go],
|
|
});
|
|
}
|
|
|
|
export interface ReadOnlyBar {
|
|
root: HTMLElement;
|
|
/** 집계표에서 고른 열 id — 없으면 null */
|
|
setColumn: (column: string | null) => void;
|
|
}
|
|
|
|
/** 회사용을 읽기만 볼 때 — 안내 + [개인용으로 복사](고른 열 하나를 내 개인 층으로) */
|
|
export function buildReadOnlyBar(user: SessionUser | null): ReadOnlyBar {
|
|
let column: string | null = null;
|
|
const copy = createButton({ label: L("M02_CopyToPersonal"), variant: "filled", size: "sm" });
|
|
copy.disabled = true;
|
|
copy.addEventListener("click", async () => {
|
|
if (!column) return showToast(L("M02_CopyPickFirst"), "error");
|
|
copy.disabled = true;
|
|
try {
|
|
const key = (await fetchStructureCombos()).줄.find((row) => row.열 === column)?.키;
|
|
if (!key) throw new Error(L("M02_CopyPickFirst"));
|
|
await copyItemToPersonal({
|
|
fromLayer: "company",
|
|
fromOwner: user?.company_id == null ? null : String(user.company_id),
|
|
key,
|
|
});
|
|
showToast(L("M02_CopyDone"), "success");
|
|
} catch (error) {
|
|
const why = error instanceof Error ? error.message : "";
|
|
showToast(L("M02_CopyFailed").replace("{value}", why), "error");
|
|
} finally {
|
|
copy.disabled = !column;
|
|
}
|
|
});
|
|
return {
|
|
root: el("div", {
|
|
className: "m02-layer-bar",
|
|
children: [el("span", { text: L("M02_ReadOnlyCompany") }), copy],
|
|
}),
|
|
setColumn: (id) => {
|
|
column = id;
|
|
copy.disabled = !id;
|
|
},
|
|
};
|
|
}
|