diff --git a/adminfront/src/features/tenants/routes/TenantCreatePage.tsx b/adminfront/src/features/tenants/routes/TenantCreatePage.tsx index f21dd5d3..3725e7a6 100644 --- a/adminfront/src/features/tenants/routes/TenantCreatePage.tsx +++ b/adminfront/src/features/tenants/routes/TenantCreatePage.tsx @@ -4,7 +4,6 @@ import { Building2, Sparkles } from "lucide-react"; import { useCallback, useMemo, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import { Button } from "../../../components/ui/button"; -import { Checkbox } from "../../../components/ui/checkbox"; import { Card, CardContent, @@ -12,6 +11,7 @@ import { CardHeader, CardTitle, } from "../../../components/ui/card"; +import { Checkbox } from "../../../components/ui/checkbox"; import { Input } from "../../../components/ui/input"; import { Label } from "../../../components/ui/label"; import { Textarea } from "../../../components/ui/textarea"; diff --git a/adminfront/src/features/tenants/routes/TenantDetailPage.tsx b/adminfront/src/features/tenants/routes/TenantDetailPage.tsx index 2a698bdf..11ef24d1 100644 --- a/adminfront/src/features/tenants/routes/TenantDetailPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantDetailPage.tsx @@ -24,8 +24,6 @@ function TenantDetailPage() { const profileRole = normalizeAdminRole(profile?.role); const canAccessSchema = profileRole === "super_admin"; - Broadway - const showWorksmobileEntry = canShowWorksmobileEntry(tenantQuery.data); const isPermissionsTab = location.pathname.includes("/permissions"); const isOrganizationTab = location.pathname.includes("/organization"); diff --git a/adminfront/src/features/tenants/routes/worksmobileAccess.ts b/adminfront/src/features/tenants/routes/worksmobileAccess.ts index 8d9a5e12..bd8a4fb1 100644 --- a/adminfront/src/features/tenants/routes/worksmobileAccess.ts +++ b/adminfront/src/features/tenants/routes/worksmobileAccess.ts @@ -19,12 +19,13 @@ export type WorksmobileAccessProfile = { }>; }; -export function isWorksmobileExcludedConfig( - config?: Record, -) { +export function isWorksmobileExcludedConfig(config?: Record) { const rawValue = config?.worksmobileExcluded; return ( - rawValue === true || String(rawValue ?? "").trim().toLowerCase() === "true" + rawValue === true || + String(rawValue ?? "") + .trim() + .toLowerCase() === "true" ); } diff --git a/adminfront/src/features/tenants/utils/orgConfig.test.ts b/adminfront/src/features/tenants/utils/orgConfig.test.ts index 3a3170c1..be722caa 100644 --- a/adminfront/src/features/tenants/utils/orgConfig.test.ts +++ b/adminfront/src/features/tenants/utils/orgConfig.test.ts @@ -80,14 +80,10 @@ describe("tenant org config", () => { }); it("reads, writes, and removes the Worksmobile exclusion flag", () => { - expect( - readTenantOrgConfig({ worksmobileExcluded: true }), - ).toMatchObject({ + expect(readTenantOrgConfig({ worksmobileExcluded: true })).toMatchObject({ worksmobileExcluded: true, }); - expect( - readTenantOrgConfig({ worksmobileExcluded: "true" }), - ).toMatchObject({ + expect(readTenantOrgConfig({ worksmobileExcluded: "true" })).toMatchObject({ worksmobileExcluded: true, }); expect( diff --git a/adminfront/src/features/users/UserCreatePage.tsx b/adminfront/src/features/users/UserCreatePage.tsx index a48c9c8d..efe8d494 100644 --- a/adminfront/src/features/users/UserCreatePage.tsx +++ b/adminfront/src/features/users/UserCreatePage.tsx @@ -986,14 +986,13 @@ function UserCreatePage() { } > - {getTenantGradeOptions( - appointment, - tenants, - ).map((grade) => ( - - ))} + {getTenantGradeOptions(appointment, tenants).map( + (grade) => ( + + ), + )}
diff --git a/backend/internal/bootstrap/keto_sync.go b/backend/internal/bootstrap/keto_sync.go index 9293dab5..417ab0f2 100644 --- a/backend/internal/bootstrap/keto_sync.go +++ b/backend/internal/bootstrap/keto_sync.go @@ -82,4 +82,3 @@ func SyncKetoRelations(db *gorm.DB, outbox repository.KetoOutboxRepository) erro slog.Info("✅ Keto ReBAC synchronization items added to Outbox.") return nil } - diff --git a/backend/internal/service/worksmobile_sync_service.go b/backend/internal/service/worksmobile_sync_service.go index b16268eb..06f39f3f 100644 --- a/backend/internal/service/worksmobile_sync_service.go +++ b/backend/internal/service/worksmobile_sync_service.go @@ -13,8 +13,10 @@ import ( "time" ) -const HanmacFamilyTenantSlug = "hanmac-family" -const worksmobileExcludedConfigKey = "worksmobileExcluded" +const ( + HanmacFamilyTenantSlug = "hanmac-family" + worksmobileExcludedConfigKey = "worksmobileExcluded" +) type WorksmobileSyncer interface { EnqueueTenantUpsertIfInScope(ctx context.Context, tenant domain.Tenant) error diff --git a/orgfront/src/features/orgchart/hanmacFamilyOrder.test.ts b/orgfront/src/features/orgchart/hanmacFamilyOrder.test.ts index f1eb6b91..0e4a2872 100644 --- a/orgfront/src/features/orgchart/hanmacFamilyOrder.test.ts +++ b/orgfront/src/features/orgchart/hanmacFamilyOrder.test.ts @@ -1,64 +1,62 @@ import { describe, expect, it } from "vitest"; import { - getHanmacFamilyTenantOrderRank, - orderHanmacFamilyChildren, - orderHanmacFamilyTenants, + getHanmacFamilyTenantOrderRank, + orderHanmacFamilyChildren, + orderHanmacFamilyTenants, } from "./hanmacFamilyOrder"; function tenant(name: string, slug: string) { - return { name, slug }; + return { name, slug }; } describe("hanmac family organization order", () => { - it("orders the top hanmac-family siblings by policy", () => { - const ordered = orderHanmacFamilyTenants([ - tenant("한라산업개발", "halla"), - tenant("바론그룹", "baron-group"), - tenant("한맥기술", "hanmac"), - tenant("삼안", "saman"), - tenant("총괄기획&기술개발센터", "gpdtdc"), - ]); + it("orders the top hanmac-family siblings by policy", () => { + const ordered = orderHanmacFamilyTenants([ + tenant("한라산업개발", "halla"), + tenant("바론그룹", "baron-group"), + tenant("한맥기술", "hanmac"), + tenant("삼안", "saman"), + tenant("총괄기획&기술개발센터", "gpdtdc"), + ]); - expect(ordered.map((item) => item.name)).toEqual([ - "총괄기획&기술개발센터", - "삼안", - "한맥기술", - "바론그룹", - "한라산업개발", - ]); - }); + expect(ordered.map((item) => item.name)).toEqual([ + "총괄기획&기술개발센터", + "삼안", + "한맥기술", + "바론그룹", + "한라산업개발", + ]); + }); - it("keeps hanmac-family as the root before ordered descendants", () => { - const family = tenant("한맥가족", "hanmac-family"); - const children = orderHanmacFamilyChildren(family, [ - tenant("바론그룹", "baron-group"), - tenant("총괄기획&기술개발센터", "gpdtdc"), - tenant("삼안", "saman"), - tenant("한라산업개발", "halla"), - tenant("한맥기술", "hanmac"), - ]); + it("keeps hanmac-family as the root before ordered descendants", () => { + const family = tenant("한맥가족", "hanmac-family"); + const children = orderHanmacFamilyChildren(family, [ + tenant("바론그룹", "baron-group"), + tenant("총괄기획&기술개발센터", "gpdtdc"), + tenant("삼안", "saman"), + tenant("한라산업개발", "halla"), + tenant("한맥기술", "hanmac"), + ]); - expect([family, ...children].map((item) => item.name)).toEqual([ - "한맥가족", - "총괄기획&기술개발센터", - "삼안", - "한맥기술", - "바론그룹", - "한라산업개발", - ]); - }); + expect([family, ...children].map((item) => item.name)).toEqual([ + "한맥가족", + "총괄기획&기술개발센터", + "삼안", + "한맥기술", + "바론그룹", + "한라산업개발", + ]); + }); - it("does not rank generic technical centers as GPDTDC", () => { - expect( - getHanmacFamilyTenantOrderRank( - tenant("기술개발센터", "rnd-center"), - ), - ).toBe(Number.MAX_SAFE_INTEGER); - }); + it("does not rank generic technical centers as GPDTDC", () => { + expect( + getHanmacFamilyTenantOrderRank(tenant("기술개발센터", "rnd-center")), + ).toBe(Number.MAX_SAFE_INTEGER); + }); - it("ranks Halla as the fifth hanmac-family company", () => { - expect( - getHanmacFamilyTenantOrderRank(tenant("한라산업개발", "halla")), - ).toBe(4); - }); + it("ranks Halla as the fifth hanmac-family company", () => { + expect( + getHanmacFamilyTenantOrderRank(tenant("한라산업개발", "halla")), + ).toBe(4); + }); }); diff --git a/orgfront/src/features/orgchart/hanmacFamilyOrder.ts b/orgfront/src/features/orgchart/hanmacFamilyOrder.ts index 50594280..d1c5543c 100644 --- a/orgfront/src/features/orgchart/hanmacFamilyOrder.ts +++ b/orgfront/src/features/orgchart/hanmacFamilyOrder.ts @@ -1,67 +1,67 @@ export type HanmacFamilyOrderTenant = { - name: string; - slug: string; + name: string; + slug: string; }; export const HANMAC_FAMILY_ROOT_SLUG = "hanmac-family"; export const HANMAC_FAMILY_TENANT_ORDER = [ - "gpdtdc", - "saman", - "hanmac", - "baron-group", - "halla", + "gpdtdc", + "saman", + "hanmac", + "baron-group", + "halla", ] as const; function normalizedTenantText(tenant: HanmacFamilyOrderTenant) { - return `${tenant.slug} ${tenant.name}`.trim().toLowerCase(); + return `${tenant.slug} ${tenant.name}`.trim().toLowerCase(); } export function isHanmacFamilyRootTenant(tenant: HanmacFamilyOrderTenant) { - return ( - tenant.slug.toLowerCase() === HANMAC_FAMILY_ROOT_SLUG || - tenant.name.includes("한맥가족") - ); + return ( + tenant.slug.toLowerCase() === HANMAC_FAMILY_ROOT_SLUG || + tenant.name.includes("한맥가족") + ); } export function getHanmacFamilyTenantOrderRank( - tenant: HanmacFamilyOrderTenant, + tenant: HanmacFamilyOrderTenant, ) { - const text = normalizedTenantText(tenant); - if (text.includes("gpdtdc") || text.includes("총괄기획")) return 0; - if (text.includes("saman") || text.includes("삼안")) return 1; - if ( - (text.includes("hanmac") || text.includes("한맥기술")) && - !isHanmacFamilyRootTenant(tenant) - ) { - return 2; - } - if (text.includes("baron-group") || text.includes("바론그룹")) return 3; - if (text.includes("halla") || text.includes("한라산업개발")) return 4; - return Number.MAX_SAFE_INTEGER; + const text = normalizedTenantText(tenant); + if (text.includes("gpdtdc") || text.includes("총괄기획")) return 0; + if (text.includes("saman") || text.includes("삼안")) return 1; + if ( + (text.includes("hanmac") || text.includes("한맥기술")) && + !isHanmacFamilyRootTenant(tenant) + ) { + return 2; + } + if (text.includes("baron-group") || text.includes("바론그룹")) return 3; + if (text.includes("halla") || text.includes("한라산업개발")) return 4; + return Number.MAX_SAFE_INTEGER; } export function compareHanmacFamilyTenants( - a: T, - b: T, + a: T, + b: T, ) { - const rankDiff = - getHanmacFamilyTenantOrderRank(a) - getHanmacFamilyTenantOrderRank(b); - if (rankDiff !== 0) return rankDiff; - return a.name.localeCompare(b.name); + const rankDiff = + getHanmacFamilyTenantOrderRank(a) - getHanmacFamilyTenantOrderRank(b); + if (rankDiff !== 0) return rankDiff; + return a.name.localeCompare(b.name); } export function orderHanmacFamilyTenants( - tenants: readonly T[], + tenants: readonly T[], ) { - return [...tenants].sort(compareHanmacFamilyTenants); + return [...tenants].sort(compareHanmacFamilyTenants); } export function orderHanmacFamilyChildren( - parent: HanmacFamilyOrderTenant, - children: readonly T[], + parent: HanmacFamilyOrderTenant, + children: readonly T[], ) { - return isHanmacFamilyRootTenant(parent) - ? orderHanmacFamilyTenants(children) - : [...children]; + return isHanmacFamilyRootTenant(parent) + ? orderHanmacFamilyTenants(children) + : [...children]; } diff --git a/orgfront/src/features/orgchart/pickerTree.test.ts b/orgfront/src/features/orgchart/pickerTree.test.ts index 5d60e445..5c48fcc6 100644 --- a/orgfront/src/features/orgchart/pickerTree.test.ts +++ b/orgfront/src/features/orgchart/pickerTree.test.ts @@ -3,237 +3,187 @@ import type { TenantSummary, UserSummary } from "../../lib/adminApi"; import { buildOrgPickerTree } from "./pickerTree"; function tenant( - id: string, - type: string, - name: string, - slug: string, - parentId?: string, + id: string, + type: string, + name: string, + slug: string, + parentId?: string, ): TenantSummary { - return { - id, - type, - name, - slug, - description: "", - status: "active", - parentId, - memberCount: 0, - createdAt: "2026-05-11T00:00:00.000Z", - updatedAt: "2026-05-11T00:00:00.000Z", - }; + return { + id, + type, + name, + slug, + description: "", + status: "active", + parentId, + memberCount: 0, + createdAt: "2026-05-11T00:00:00.000Z", + updatedAt: "2026-05-11T00:00:00.000Z", + }; } describe("buildOrgPickerTree", () => { - it("uses the hanmac-family company-group as the default picker root", () => { - const tenants = [ - tenant( - "wrong-group", - "COMPANY_GROUP", - "Wrong Group", - "wrong-group", - ), - tenant( - "wrong-company", - "COMPANY", - "Wrong Company", - "wrong-company", - "wrong-group", - ), - tenant( - "hanmac-family-id", - "COMPANY_GROUP", - "한맥가족", - "hanmac-family", - ), - tenant("saman-id", "COMPANY", "삼안", "saman", "hanmac-family-id"), - ]; + it("uses the hanmac-family company-group as the default picker root", () => { + const tenants = [ + tenant("wrong-group", "COMPANY_GROUP", "Wrong Group", "wrong-group"), + tenant( + "wrong-company", + "COMPANY", + "Wrong Company", + "wrong-company", + "wrong-group", + ), + tenant("hanmac-family-id", "COMPANY_GROUP", "한맥가족", "hanmac-family"), + tenant("saman-id", "COMPANY", "삼안", "saman", "hanmac-family-id"), + ]; - const tree = buildOrgPickerTree({ - tenants, - users: [] satisfies UserSummary[], - }); - - expect(tree.companyGroupId).toBe("hanmac-family-id"); - expect(tree.roots).toHaveLength(1); - expect(tree.roots[0]?.id).toBe("hanmac-family-id"); - expect(tree.roots[0]?.children.map((node) => node.id)).toEqual([ - "saman-id", - ]); + const tree = buildOrgPickerTree({ + tenants, + users: [] satisfies UserSummary[], }); - it("orders hanmac-family children by the shared organization policy", () => { - const tenants = [ - tenant( - "hanmac-family-id", - "COMPANY_GROUP", - "한맥가족", - "hanmac-family", - ), - tenant( - "baron-group-id", - "COMPANY_GROUP", - "바론그룹", - "baron-group", - "hanmac-family-id", - ), - tenant( - "hanmac-id", - "COMPANY", - "한맥기술", - "hanmac", - "hanmac-family-id", - ), - tenant( - "halla-id", - "COMPANY", - "한라산업개발", - "halla", - "hanmac-family-id", - ), - tenant("saman-id", "COMPANY", "삼안", "saman", "hanmac-family-id"), - tenant( - "gpdtdc-id", - "ORGANIZATION", - "총괄기획&기술개발센터", - "gpdtdc", - "hanmac-family-id", - ), - ]; + expect(tree.companyGroupId).toBe("hanmac-family-id"); + expect(tree.roots).toHaveLength(1); + expect(tree.roots[0]?.id).toBe("hanmac-family-id"); + expect(tree.roots[0]?.children.map((node) => node.id)).toEqual([ + "saman-id", + ]); + }); - const tree = buildOrgPickerTree({ - tenants, - users: [] satisfies UserSummary[], - }); + it("orders hanmac-family children by the shared organization policy", () => { + const tenants = [ + tenant("hanmac-family-id", "COMPANY_GROUP", "한맥가족", "hanmac-family"), + tenant( + "baron-group-id", + "COMPANY_GROUP", + "바론그룹", + "baron-group", + "hanmac-family-id", + ), + tenant("hanmac-id", "COMPANY", "한맥기술", "hanmac", "hanmac-family-id"), + tenant( + "halla-id", + "COMPANY", + "한라산업개발", + "halla", + "hanmac-family-id", + ), + tenant("saman-id", "COMPANY", "삼안", "saman", "hanmac-family-id"), + tenant( + "gpdtdc-id", + "ORGANIZATION", + "총괄기획&기술개발센터", + "gpdtdc", + "hanmac-family-id", + ), + ]; - expect(tree.roots[0]?.children.map((node) => node.name)).toEqual([ - "총괄기획&기술개발센터", - "삼안", - "한맥기술", - "바론그룹", - "한라산업개발", - ]); + const tree = buildOrgPickerTree({ + tenants, + users: [] satisfies UserSummary[], }); - it("scopes descendant filtering by tenant slug", () => { - const tenants = [ - tenant( - "hanmac-family-id", - "COMPANY_GROUP", - "한맥가족", - "hanmac-family", - ), - tenant("saman-id", "COMPANY", "삼안", "saman", "hanmac-family-id"), - tenant( - "planning-id", - "ORGANIZATION", - "기획팀", - "planning", - "saman-id", - ), - tenant( - "hanmac-id", - "COMPANY", - "한맥기술", - "hanmac", - "hanmac-family-id", - ), - ]; + expect(tree.roots[0]?.children.map((node) => node.name)).toEqual([ + "총괄기획&기술개발센터", + "삼안", + "한맥기술", + "바론그룹", + "한라산업개발", + ]); + }); - const tree = buildOrgPickerTree({ - tenants, - users: [] satisfies UserSummary[], - tenantId: "saman", - }); + it("scopes descendant filtering by tenant slug", () => { + const tenants = [ + tenant("hanmac-family-id", "COMPANY_GROUP", "한맥가족", "hanmac-family"), + tenant("saman-id", "COMPANY", "삼안", "saman", "hanmac-family-id"), + tenant("planning-id", "ORGANIZATION", "기획팀", "planning", "saman-id"), + tenant("hanmac-id", "COMPANY", "한맥기술", "hanmac", "hanmac-family-id"), + ]; - expect(tree.roots).toHaveLength(1); - expect(tree.roots[0]?.id).toBe("saman-id"); - expect(tree.roots[0]?.children.map((node) => node.id)).toEqual([ - "planning-id", - ]); + const tree = buildOrgPickerTree({ + tenants, + users: [] satisfies UserSummary[], + tenantId: "saman", }); - it("excludes internal and private tenants from picker choices by default", () => { - const tenants = [ - tenant( - "hanmac-family-id", - "COMPANY_GROUP", - "한맥가족", - "hanmac-family", - ), - tenant("saman-id", "COMPANY", "삼안", "saman", "hanmac-family-id"), - { - ...tenant( - "internal-id", - "ORGANIZATION", - "내부 조직", - "internal", - "saman-id", - ), - config: { visibility: "internal" }, - }, - { - ...tenant( - "secret-id", - "ORGANIZATION", - "비공개 조직", - "secret", - "saman-id", - ), - config: { visibility: "private" }, - }, - tenant( - "secret-child-id", - "USER_GROUP", - "비공개 하위", - "secret-child", - "secret-id", - ), - tenant("open-id", "ORGANIZATION", "공개 조직", "open", "saman-id"), - ]; + expect(tree.roots).toHaveLength(1); + expect(tree.roots[0]?.id).toBe("saman-id"); + expect(tree.roots[0]?.children.map((node) => node.id)).toEqual([ + "planning-id", + ]); + }); - const tree = buildOrgPickerTree({ - tenants, - users: [] satisfies UserSummary[], - tenantId: "saman", - }); + it("excludes internal and private tenants from picker choices by default", () => { + const tenants = [ + tenant("hanmac-family-id", "COMPANY_GROUP", "한맥가족", "hanmac-family"), + tenant("saman-id", "COMPANY", "삼안", "saman", "hanmac-family-id"), + { + ...tenant( + "internal-id", + "ORGANIZATION", + "내부 조직", + "internal", + "saman-id", + ), + config: { visibility: "internal" }, + }, + { + ...tenant( + "secret-id", + "ORGANIZATION", + "비공개 조직", + "secret", + "saman-id", + ), + config: { visibility: "private" }, + }, + tenant( + "secret-child-id", + "USER_GROUP", + "비공개 하위", + "secret-child", + "secret-id", + ), + tenant("open-id", "ORGANIZATION", "공개 조직", "open", "saman-id"), + ]; - expect(tree.roots[0]?.children.map((node) => node.id)).toEqual([ - "open-id", - ]); + const tree = buildOrgPickerTree({ + tenants, + users: [] satisfies UserSummary[], + tenantId: "saman", }); - it("includes internal tenants when explicitly requested", () => { - const tenants = [ - tenant( - "hanmac-family-id", - "COMPANY_GROUP", - "한맥가족", - "hanmac-family", - ), - tenant("saman-id", "COMPANY", "삼안", "saman", "hanmac-family-id"), - { - ...tenant( - "internal-id", - "ORGANIZATION", - "내부 조직", - "internal", - "saman-id", - ), - config: { visibility: "internal" }, - }, - tenant("open-id", "ORGANIZATION", "공개 조직", "open", "saman-id"), - ]; + expect(tree.roots[0]?.children.map((node) => node.id)).toEqual(["open-id"]); + }); - const tree = buildOrgPickerTree({ - includeInternal: true, - tenants, - users: [] satisfies UserSummary[], - tenantId: "saman", - }); + it("includes internal tenants when explicitly requested", () => { + const tenants = [ + tenant("hanmac-family-id", "COMPANY_GROUP", "한맥가족", "hanmac-family"), + tenant("saman-id", "COMPANY", "삼안", "saman", "hanmac-family-id"), + { + ...tenant( + "internal-id", + "ORGANIZATION", + "내부 조직", + "internal", + "saman-id", + ), + config: { visibility: "internal" }, + }, + tenant("open-id", "ORGANIZATION", "공개 조직", "open", "saman-id"), + ]; - expect(tree.roots[0]?.children.map((node) => node.id)).toEqual([ - "internal-id", - "open-id", - ]); + const tree = buildOrgPickerTree({ + includeInternal: true, + tenants, + users: [] satisfies UserSummary[], + tenantId: "saman", }); + + expect(tree.roots[0]?.children.map((node) => node.id)).toEqual([ + "internal-id", + "open-id", + ]); + }); });