import { NgTemplateOutlet } from "@angular/common"; import { booleanAttribute, ChangeDetectionStrategy, Component, computed, input, model, signal, ViewEncapsulation, } from "@angular/core"; import { obj } from "@simplysm/core-common"; import type { SdPermission } from "../../core/app-structure/sd-app-structure.types"; import { SdCheckbox } from "../../controls/checkbox/sd-checkbox"; import { SdCollapseIcon } from "../../controls/collapse/sd-collapse-icon"; import { SdTypedTemplate } from "../../core/template/sd-typed-template"; import { SdAnchor } from "../../controls/button/sd-anchor"; @Component({ selector: "sd-permission-table", changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, standalone: true, imports: [ SdTypedTemplate, NgTemplateOutlet, SdCollapseIcon, SdCheckbox, SdAnchor, ], styles: [ /* language=SCSS */ ` sd-permission-table { table { border-collapse: collapse; > * > tr { > * { padding: var(--gap-sm) var(--gap-lg); position: sticky; top: 0; border-top: 1px solid transparent; border-bottom: 1px solid transparent; color: var(--text-trans-default); > * { color: var(--text-trans-default) !important; } &._title { border-top-left-radius: 14px; border-bottom-left-radius: 14px; padding-left: var(--gap-lg); } } &[data-sd-collapse="true"] { display: none; } &[data-sd-theme="first"] { > * { &._title, &._after { background: var(--theme-info-default); color: var(--text-trans-rev-default); > * { color: var(--text-trans-rev-default) !important; } } } } &[data-sd-theme="info"] { > * { &._title, &._after { background: var(--theme-info-lightest); } } } &[data-sd-theme="warning"] { > * { &._title, &._after { background: var(--theme-warning-lightest); } } } &[data-sd-theme="success"] { > * { &._title, &._after { background: var(--theme-success-lightest); } } } } } } `, ], template: ` @for (item of items(); track item.codeChain.join(".")) { }
@if ( (item.children && item.children.length !== 0) || (item.perms && item.perms.length > 0) ) { @for (i of arr(depth + 1); track i) {   } @if (item.children && item.children.length > 0) { {{ item.title }} } @else {
{{ item.title }}
} @for (i of arr(depthLength() - (depth + 1)); track i) {   } @if (getIsPermExists(item, "use")) { 사용 } @if (getIsPermExists(item, "edit")) { 편집 } } @if (item.children && item.children.length > 0) { @for (child of item.children; track child.codeChain.join(".")) { } }
`, }) export class SdPermissionTable { value = model>({}); items = input[]>([]); disabled = input(false, { transform: booleanAttribute }); collapsedItems = signal(new Set()); depthLength = computed(() => { return this._getDepthLength(this.items(), 0); }); private readonly _permExistsCache = computed(() => { const cache = new Map(); const walk = (item: SdPermission, type: "use" | "edit"): boolean => { const key = item.codeChain.join(".") + "." + type; const cached = cache.get(key); if (cached != null) return cached; if (item.perms) { if (item.children) { for (const child of item.children) { walk(child, type); } } const result = item.perms.includes(type); cache.set(key, result); return result; } let result = false; if (item.children) { for (const child of item.children) { if (walk(child, type)) { result = true; } } } cache.set(key, result); return result; }; for (const item of this.items()) { walk(item, "use"); walk(item, "edit"); } return cache; }); private readonly _permCheckedCache = computed(() => { const cache = new Map(); const value = this.value(); const walk = (item: SdPermission, type: "use" | "edit"): boolean => { const key = item.codeChain.join(".") + "." + type; const cached = cache.get(key); if (cached != null) return cached; if (item.perms) { if (item.children) { for (const child of item.children) { walk(child, type); } } const permCode = item.codeChain.join("."); const result = value[permCode + "." + type] ?? false; cache.set(key, result); return result; } let result = false; if (item.children) { for (const child of item.children) { if (walk(child, type)) { result = true; } } } cache.set(key, result); return result; }; for (const item of this.items()) { walk(item, "use"); walk(item, "edit"); } return cache; }); private readonly _editDisabledCache = computed(() => { const cache = new Map(); const isDisabled = this.disabled(); const existsCache = this._permExistsCache(); const checkedCache = this._permCheckedCache(); const walk = (item: SdPermission): boolean => { const key = item.codeChain.join("."); const cached = cache.get(key); if (cached != null) return cached; if (isDisabled) { if (item.children) { for (const child of item.children) { walk(child); } } cache.set(key, true); return true; } if (item.perms) { const useExists = existsCache.get(key + ".use") ?? false; const useChecked = checkedCache.get(key + ".use") ?? false; const result = useExists && !useChecked; cache.set(key, result); return result; } if (item.children) { for (const child of item.children) { walk(child); } const result = item.children.every( (child) => !(existsCache.get(child.codeChain.join(".") + ".edit") ?? false) || (cache.get(child.codeChain.join(".")) ?? false), ); cache.set(key, result); return result; } cache.set(key, false); return false; }; for (const item of this.items()) { walk(item); } return cache; }); private readonly _arrCache = new Map(); arr(len: number): number[] { let cached = this._arrCache.get(len); if (cached == null) { cached = Array(len) .fill(0) .map((_, i) => i); this._arrCache.set(len, cached); } return cached; } getIsPermCollapsed(item: SdPermission): boolean { return this.collapsedItems().has(item.codeChain.join(".")); } getAllChildren(item: SdPermission): SdPermission[] { return item.children?.mapMany((child) => [child, ...this.getAllChildren(child)]) ?? []; } getEditDisabled(item: SdPermission): boolean { return this._editDisabledCache().get(item.codeChain.join(".")) ?? false; } getIsPermExists(item: SdPermission, type: "use" | "edit"): boolean { return this._permExistsCache().get(item.codeChain.join(".") + "." + type) ?? false; } getIsPermChecked(item: SdPermission, type: "use" | "edit"): boolean { return this._permCheckedCache().get(item.codeChain.join(".") + "." + type) ?? false; } onPermCollapseToggle(item: SdPermission) { const key = item.codeChain.join("."); this.collapsedItems.update((v) => { const r = new Set(v); if (r.has(key)) { r.delete(key); } else { r.add(key); const allChildren = this.getAllChildren(item); for (const allChild of allChildren) { r.add(allChild.codeChain.join(".")); } } return r; }); } onPermCheckChange(item: SdPermission, type: "use" | "edit", val: boolean) { this.value.update((v) => { const r = obj.clone(v); this._changePermCheck(r, item, type, val); return r; }); } private _changePermCheck( value: Record, item: SdPermission, type: "use" | "edit", val: boolean, ) { let changed = false; if (item.perms) { const permCode = item.codeChain.join("."); if ( type === "edit" && val && this.getIsPermExists(item, "use") && !(value[permCode + ".use"] ?? false) ) { // use가 체크되지 않은 상태에서 edit 체크 시도 → 무시 } else { if (this.getIsPermExists(item, type) && value[permCode + "." + type] !== val) { value[permCode + "." + type] = val; changed = true; } } // USE권한 지우면 EDIT권한도 자동으로 지움 if ( type === "use" && !val && this.getIsPermExists(item, "edit") && value[permCode + ".edit"] ) { value[permCode + ".edit"] = false; changed = true; } } // 하위 권한을 함께 변경함 if (item.children) { for (const child of item.children) { const childChanged = this._changePermCheck(value, child, type, val); if (childChanged) { changed = true; } } } return changed; } private _getDepthLength(items: SdPermission[], depth: number): number { return ( items.max((item) => { if (item.children) { return this._getDepthLength(item.children, depth + 1); } else { return depth + 1; } }) ?? depth ); } protected readonly itemTemplateType!: { item: SdPermission; parentKey: string; depth: number; parent: SdPermission | undefined; }; }