forked from baron/baron-sso
테넌트 목록 조회 cursor기반으로 재구성. 사용자 metadata 미사용 필드 제거
This commit is contained in:
@@ -42,11 +42,10 @@ import {
|
||||
type UserAppointment,
|
||||
type UserCreateRequest,
|
||||
type UserCreateResponse,
|
||||
createTenant,
|
||||
createUser,
|
||||
fetchAllTenants,
|
||||
fetchMe,
|
||||
fetchTenant,
|
||||
fetchTenants,
|
||||
} from "../../lib/adminApi";
|
||||
import { t } from "../../lib/i18n";
|
||||
import {
|
||||
@@ -56,9 +55,10 @@ import {
|
||||
parseOrgChartTenantSelection,
|
||||
} from "./orgChartPicker";
|
||||
import type { UserSchemaField } from "./userSchemaFields";
|
||||
import { resolvePersonalTenant } from "./utils/personalTenant";
|
||||
|
||||
type UserFormValues = UserCreateRequest & { metadata: Record<string, unknown> };
|
||||
type UserType = "hanmac" | "external" | "personal";
|
||||
type UserCategory = "hanmac" | "external" | "personal";
|
||||
|
||||
type PickerTarget = { kind: "appointment"; index: number };
|
||||
|
||||
@@ -114,8 +114,8 @@ function UserCreatePage() {
|
||||
>(null);
|
||||
const [createdEmail, setCreatedEmail] = React.useState<string | null>(null);
|
||||
const [autoPassword, setAutoPassword] = React.useState(true);
|
||||
const [isHanmacFamily, setIsHanmacFamily] = React.useState(true);
|
||||
const [userType, setUserType] = React.useState<UserType>("hanmac");
|
||||
const [userCategory, setUserCategory] =
|
||||
React.useState<UserCategory>("hanmac");
|
||||
const [additionalAppointments, setAdditionalAppointments] = React.useState<
|
||||
AppointmentDraft[]
|
||||
>([]);
|
||||
@@ -125,8 +125,8 @@ function UserCreatePage() {
|
||||
const [isResolvingTenant, setIsResolvingTenant] = React.useState(false);
|
||||
|
||||
const { data: tenantsData } = useQuery({
|
||||
queryKey: ["tenants", { limit: 100 }],
|
||||
queryFn: () => fetchTenants(100, 0),
|
||||
queryKey: ["tenants", "all"],
|
||||
queryFn: () => fetchAllTenants(),
|
||||
});
|
||||
const tenants = tenantsData?.items ?? [];
|
||||
|
||||
@@ -177,17 +177,11 @@ function UserCreatePage() {
|
||||
|
||||
const selectedTenantSlug = watch("tenantSlug");
|
||||
const personalTenant = React.useMemo(
|
||||
() =>
|
||||
tenants.find(
|
||||
(tenant) =>
|
||||
tenant.slug === "personal" ||
|
||||
(tenant.type === "PERSONAL" &&
|
||||
tenant.name.toLowerCase() === "personal"),
|
||||
),
|
||||
() => resolvePersonalTenant(tenants),
|
||||
[tenants],
|
||||
);
|
||||
const selectedTenant =
|
||||
userType !== "external"
|
||||
userCategory !== "external"
|
||||
? undefined
|
||||
: nonHanmacFamilyTenants.find((t) => t.slug === selectedTenantSlug);
|
||||
|
||||
@@ -231,7 +225,7 @@ function UserCreatePage() {
|
||||
const pickerUrl = buildAuthenticatedOrgChartTenantPickerUrl(
|
||||
import.meta.env.ORGFRONT_URL,
|
||||
{
|
||||
tenantId: userType === "hanmac" ? hanmacFamilyTenantId : undefined,
|
||||
tenantId: userCategory === "hanmac" ? hanmacFamilyTenantId : undefined,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -310,25 +304,16 @@ function UserCreatePage() {
|
||||
);
|
||||
};
|
||||
|
||||
const handleUserTypeChange = (value: string) => {
|
||||
const nextType = value as UserType;
|
||||
setUserType(nextType);
|
||||
setIsHanmacFamily(nextType === "hanmac");
|
||||
if (nextType !== "hanmac") {
|
||||
const handleUserCategoryChange = (value: string) => {
|
||||
const nextCategory = value as UserCategory;
|
||||
setUserCategory(nextCategory);
|
||||
if (nextCategory !== "hanmac") {
|
||||
setAdditionalAppointments([]);
|
||||
}
|
||||
};
|
||||
|
||||
const ensurePersonalTenant = async () => {
|
||||
if (personalTenant) return personalTenant;
|
||||
const tenant = await createTenant({
|
||||
name: "Personal",
|
||||
slug: "personal",
|
||||
type: "PERSONAL",
|
||||
status: "active",
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ["tenants"] });
|
||||
return tenant;
|
||||
return personalTenant;
|
||||
};
|
||||
|
||||
const mutation = useMutation({
|
||||
@@ -355,10 +340,13 @@ function UserCreatePage() {
|
||||
setGeneratedPassword(null);
|
||||
setCreatedEmail(null);
|
||||
|
||||
const {
|
||||
hanmacFamily: _hanmacFamily,
|
||||
userType: _userType,
|
||||
...formMetadata
|
||||
} = data.metadata ?? {};
|
||||
const metadata: Record<string, unknown> = {
|
||||
...(data.metadata ?? {}),
|
||||
hanmacFamily: userType === "hanmac" && isHanmacFamily,
|
||||
userType,
|
||||
...formMetadata,
|
||||
};
|
||||
|
||||
const payload: UserCreateRequest = {
|
||||
@@ -369,7 +357,7 @@ function UserCreatePage() {
|
||||
metadata,
|
||||
};
|
||||
|
||||
if (userType === "external") {
|
||||
if (userCategory === "external") {
|
||||
if (!data.tenantSlug) {
|
||||
setError(
|
||||
t(
|
||||
@@ -386,7 +374,7 @@ function UserCreatePage() {
|
||||
payload.jobTitle = data.jobTitle;
|
||||
}
|
||||
|
||||
if (userType === "personal") {
|
||||
if (userCategory === "personal") {
|
||||
try {
|
||||
const tenant = await ensurePersonalTenant();
|
||||
payload.tenantSlug = tenant.slug;
|
||||
@@ -405,7 +393,7 @@ function UserCreatePage() {
|
||||
}
|
||||
}
|
||||
|
||||
if (userType === "hanmac") {
|
||||
if (userCategory === "hanmac") {
|
||||
const appointments = additionalAppointments
|
||||
.filter((appointment) => appointment.tenantId)
|
||||
.map((appointment) => ({
|
||||
@@ -644,7 +632,7 @@ function UserCreatePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs value={userType} onValueChange={handleUserTypeChange}>
|
||||
<Tabs value={userCategory} onValueChange={handleUserCategoryChange}>
|
||||
<TabsList className="flex h-auto w-full justify-start rounded-none border-b bg-transparent p-0 text-foreground">
|
||||
<TabsTrigger
|
||||
value="hanmac"
|
||||
|
||||
@@ -56,12 +56,11 @@ import {
|
||||
type TenantSummary,
|
||||
type UserAppointment,
|
||||
type UserUpdateRequest,
|
||||
createTenant,
|
||||
deleteUser,
|
||||
fetchAllTenants,
|
||||
fetchMe,
|
||||
fetchPasswordPolicy,
|
||||
fetchTenant,
|
||||
fetchTenants,
|
||||
fetchUser,
|
||||
fetchUserRpHistory,
|
||||
updateUser,
|
||||
@@ -78,11 +77,12 @@ import {
|
||||
parseOrgChartTenantSelection,
|
||||
} from "./orgChartPicker";
|
||||
import type { UserSchemaField } from "./userSchemaFields";
|
||||
import { resolvePersonalTenant } from "./utils/personalTenant";
|
||||
|
||||
type UserFormValues = Omit<UserUpdateRequest, "metadata"> & {
|
||||
metadata: Record<string, Record<string, string | number | boolean>>;
|
||||
};
|
||||
type UserType = "hanmac" | "external" | "personal";
|
||||
type UserCategory = "hanmac" | "external" | "personal";
|
||||
|
||||
type PasswordResetMode = "generated" | "manual";
|
||||
type PickerTarget = { kind: "appointment"; index: number };
|
||||
@@ -318,8 +318,8 @@ function UserDetailPage() {
|
||||
const [passwordResetError, setPasswordResetError] = React.useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [isHanmacFamily, setIsHanmacFamily] = React.useState(false);
|
||||
const [userType, setUserType] = React.useState<UserType>("external");
|
||||
const [userCategory, setUserCategory] =
|
||||
React.useState<UserCategory>("external");
|
||||
const [additionalAppointments, setAdditionalAppointments] = React.useState<
|
||||
AppointmentDraft[]
|
||||
>([]);
|
||||
@@ -346,8 +346,8 @@ function UserDetailPage() {
|
||||
});
|
||||
|
||||
const { data: tenantsData } = useQuery({
|
||||
queryKey: ["tenants", { limit: 100 }],
|
||||
queryFn: () => fetchTenants(100, 0),
|
||||
queryKey: ["tenants", "all"],
|
||||
queryFn: () => fetchAllTenants(),
|
||||
});
|
||||
const tenants = React.useMemo(
|
||||
() => tenantsData?.items ?? [],
|
||||
@@ -465,20 +465,14 @@ function UserDetailPage() {
|
||||
return tenants.find((tenant) => tenant.slug === "hanmac-family")?.id ?? "";
|
||||
}, [tenants]);
|
||||
const personalTenant = React.useMemo(
|
||||
() =>
|
||||
tenants.find(
|
||||
(tenant) =>
|
||||
tenant.slug === "personal" ||
|
||||
(tenant.type === "PERSONAL" &&
|
||||
tenant.name.toLowerCase() === "personal"),
|
||||
),
|
||||
() => resolvePersonalTenant(tenants),
|
||||
[tenants],
|
||||
);
|
||||
|
||||
const pickerUrl = buildAuthenticatedOrgChartTenantPickerUrl(
|
||||
import.meta.env.ORGFRONT_URL,
|
||||
{
|
||||
tenantId: userType === "hanmac" ? hanmacFamilyTenantId : undefined,
|
||||
tenantId: userCategory === "hanmac" ? hanmacFamilyTenantId : undefined,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -566,25 +560,16 @@ function UserDetailPage() {
|
||||
);
|
||||
};
|
||||
|
||||
const handleUserTypeChange = (value: string) => {
|
||||
const nextType = value as UserType;
|
||||
setUserType(nextType);
|
||||
setIsHanmacFamily(nextType === "hanmac");
|
||||
if (nextType !== "hanmac") {
|
||||
const handleUserCategoryChange = (value: string) => {
|
||||
const nextCategory = value as UserCategory;
|
||||
setUserCategory(nextCategory);
|
||||
if (nextCategory !== "hanmac") {
|
||||
setAdditionalAppointments([]);
|
||||
}
|
||||
};
|
||||
|
||||
const ensurePersonalTenant = async () => {
|
||||
if (personalTenant) return personalTenant;
|
||||
const tenant = await createTenant({
|
||||
name: "Personal",
|
||||
slug: "personal",
|
||||
type: "PERSONAL",
|
||||
status: "active",
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ["tenants"] });
|
||||
return tenant;
|
||||
return personalTenant;
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -638,14 +623,18 @@ function UserDetailPage() {
|
||||
tenants,
|
||||
hanmacFamilyTenantId,
|
||||
);
|
||||
const resolvedUserType =
|
||||
metadata.userType === "personal" || user.companyCode === "personal"
|
||||
? "personal"
|
||||
: isUserHanmacFamily
|
||||
? "hanmac"
|
||||
: "external";
|
||||
setUserType(resolvedUserType);
|
||||
setIsHanmacFamily(resolvedUserType === "hanmac");
|
||||
const isPersonalUser =
|
||||
user.companyCode === personalTenant.slug ||
|
||||
user.tenantSlug === personalTenant.slug ||
|
||||
user.tenant?.id === personalTenant.id ||
|
||||
user.tenant?.slug === personalTenant.slug ||
|
||||
metadata.personalTenantId === personalTenant.id;
|
||||
const resolvedUserCategory = isPersonalUser
|
||||
? "personal"
|
||||
: isUserHanmacFamily
|
||||
? "hanmac"
|
||||
: "external";
|
||||
setUserCategory(resolvedUserCategory);
|
||||
const familyFallbackTenants = [
|
||||
...(user.joinedTenants ?? []),
|
||||
...(user.tenant ? [user.tenant] : []),
|
||||
@@ -696,7 +685,7 @@ function UserDetailPage() {
|
||||
: [],
|
||||
);
|
||||
}
|
||||
}, [hanmacFamilyTenantId, tenants, user, reset]);
|
||||
}, [hanmacFamilyTenantId, personalTenant, tenants, user, reset]);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (data: UserUpdateRequest) => updateUser(userId, data),
|
||||
@@ -737,10 +726,13 @@ function UserDetailPage() {
|
||||
}),
|
||||
);
|
||||
|
||||
const {
|
||||
hanmacFamily: _hanmacFamily,
|
||||
userType: _userType,
|
||||
...safeMetadata
|
||||
} = cleanMetadata;
|
||||
const metadata: Record<string, unknown> = {
|
||||
...cleanMetadata,
|
||||
hanmacFamily: userType === "hanmac" && isHanmacFamily,
|
||||
userType,
|
||||
...safeMetadata,
|
||||
};
|
||||
|
||||
const profileData = { ...data };
|
||||
@@ -750,7 +742,7 @@ function UserDetailPage() {
|
||||
metadata,
|
||||
};
|
||||
|
||||
if (userType === "personal") {
|
||||
if (userCategory === "personal") {
|
||||
try {
|
||||
const tenant = await ensurePersonalTenant();
|
||||
payload.tenantSlug = tenant.slug;
|
||||
@@ -768,7 +760,7 @@ function UserDetailPage() {
|
||||
}
|
||||
}
|
||||
|
||||
if (userType === "hanmac") {
|
||||
if (userCategory === "hanmac") {
|
||||
const appointments = additionalAppointments
|
||||
.filter((appointment) => appointment.tenantId)
|
||||
.map((appointment) => ({
|
||||
@@ -1071,8 +1063,8 @@ function UserDetailPage() {
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
value={userType}
|
||||
onValueChange={handleUserTypeChange}
|
||||
value={userCategory}
|
||||
onValueChange={handleUserCategoryChange}
|
||||
className="space-y-4 pt-6 border-t border-dashed"
|
||||
>
|
||||
<TabsList className="flex h-auto w-full justify-start rounded-none border-b bg-transparent p-0 text-foreground">
|
||||
@@ -1097,7 +1089,7 @@ function UserDetailPage() {
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
{userType === "external" && (
|
||||
{userCategory === "external" && (
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label
|
||||
@@ -1141,7 +1133,7 @@ function UserDetailPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{userType === "hanmac" && (
|
||||
{userCategory === "hanmac" && (
|
||||
<div className="space-y-4 rounded-md border p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
@@ -1314,7 +1306,7 @@ function UserDetailPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{userType === "personal" && (
|
||||
{userCategory === "personal" && (
|
||||
<div className="rounded-md border bg-muted/30 p-4 text-sm">
|
||||
{personalTenant
|
||||
? `Personal (${personalTenant.slug})`
|
||||
@@ -1322,7 +1314,7 @@ function UserDetailPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{userType === "external" && (
|
||||
{userCategory === "external" && (
|
||||
<div className="grid gap-6 md:grid-cols-3 pt-8 border-t">
|
||||
<div className="space-y-2">
|
||||
<Label
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
RefreshCw,
|
||||
Search,
|
||||
Settings2,
|
||||
ShieldCheck,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
@@ -62,9 +63,9 @@ import {
|
||||
bulkUpdateUsers,
|
||||
deleteUser,
|
||||
exportUsersCSV,
|
||||
fetchAllTenants,
|
||||
fetchMe,
|
||||
fetchTenant,
|
||||
fetchTenants,
|
||||
fetchUsers,
|
||||
updateUser,
|
||||
} from "../../lib/adminApi";
|
||||
@@ -101,8 +102,8 @@ function UserListPage() {
|
||||
});
|
||||
|
||||
const { data: tenantsData } = useQuery({
|
||||
queryKey: ["tenants", { limit: 100 }],
|
||||
queryFn: () => fetchTenants(100, 0),
|
||||
queryKey: ["tenants", "all"],
|
||||
queryFn: () => fetchAllTenants(),
|
||||
});
|
||||
const tenants = tenantsData?.items ?? [];
|
||||
|
||||
@@ -269,6 +270,7 @@ function UserListPage() {
|
||||
|
||||
const total = query.data?.total ?? 0;
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
const canPromoteSuperAdmin = profile?.role === "super_admin";
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
if (selectedUserIds.length === items.length) {
|
||||
@@ -318,6 +320,14 @@ function UserListPage() {
|
||||
bulkUpdateMutation.mutate({ userIds: selectedUserIds, status });
|
||||
};
|
||||
|
||||
const handlePromoteSuperAdmin = () => {
|
||||
if (selectedUserIds.length === 0) return;
|
||||
bulkUpdateMutation.mutate({
|
||||
userIds: selectedUserIds,
|
||||
role: "super_admin",
|
||||
});
|
||||
};
|
||||
|
||||
const handleBulkDelete = () => {
|
||||
if (selectedUserIds.length === 0) return;
|
||||
if (
|
||||
@@ -774,6 +784,18 @@ function UserListPage() {
|
||||
>
|
||||
{t("ui.common.status.inactive", "비활성화")}
|
||||
</Button>
|
||||
{canPromoteSuperAdmin && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-background hover:bg-background/10 h-8 gap-1.5"
|
||||
onClick={handlePromoteSuperAdmin}
|
||||
data-testid="bulk-promote-super-admin-btn"
|
||||
>
|
||||
<ShieldCheck size={14} />
|
||||
{t("ui.admin.users.bulk.promote_admin", "Admin으로 만들기")}
|
||||
</Button>
|
||||
)}
|
||||
<div className="w-px h-4 bg-background/20 mx-1" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -20,8 +20,8 @@ import {
|
||||
type TenantSummary,
|
||||
type UserSummary,
|
||||
bulkUpdateUsers,
|
||||
fetchAllTenants,
|
||||
fetchGroups,
|
||||
fetchTenants,
|
||||
} from "../../../lib/adminApi";
|
||||
import { t } from "../../../lib/i18n";
|
||||
|
||||
@@ -52,8 +52,8 @@ export function UserBulkMoveGroupModal({
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: tenantsData } = useQuery({
|
||||
queryKey: ["tenants", { limit: 100 }],
|
||||
queryFn: () => fetchTenants(100, 0),
|
||||
queryKey: ["tenants", "all"],
|
||||
queryFn: () => fetchAllTenants(),
|
||||
enabled: open,
|
||||
});
|
||||
const tenants = tenantsData?.items ?? [];
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
type BulkUserResult,
|
||||
bulkCreateUsers,
|
||||
createTenant,
|
||||
fetchTenants,
|
||||
fetchAllTenants,
|
||||
fetchUsers,
|
||||
} from "../../../lib/adminApi";
|
||||
import { t } from "../../../lib/i18n";
|
||||
@@ -139,7 +139,7 @@ export function UserBulkUploadModal({ onSuccess }: UserBulkUploadModalProps) {
|
||||
|
||||
const tenantQuery = useQuery({
|
||||
queryKey: ["tenants", "user-bulk-import"],
|
||||
queryFn: () => fetchTenants(1000, 0),
|
||||
queryFn: () => fetchAllTenants(),
|
||||
});
|
||||
|
||||
const usersQuery = useQuery({
|
||||
|
||||
@@ -169,4 +169,66 @@ describe("orgChartPicker", () => {
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not treat legacy hanmacFamily metadata as Hanmac family without tenant evidence", () => {
|
||||
const tenants = [
|
||||
{
|
||||
id: "hanmac-family-id",
|
||||
slug: "hanmac-family",
|
||||
name: "한맥가족",
|
||||
type: "COMPANY_GROUP",
|
||||
parentId: undefined,
|
||||
},
|
||||
{
|
||||
id: "external-id",
|
||||
slug: "external",
|
||||
name: "External",
|
||||
type: "COMPANY",
|
||||
parentId: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
expect(
|
||||
isHanmacFamilyUser(
|
||||
{
|
||||
companyCode: "external",
|
||||
tenant: tenants[1],
|
||||
metadata: { hanmacFamily: true },
|
||||
},
|
||||
tenants,
|
||||
"hanmac-family-id",
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not treat userType metadata as Hanmac family without tenant evidence", () => {
|
||||
const tenants = [
|
||||
{
|
||||
id: "hanmac-family-id",
|
||||
slug: "hanmac-family",
|
||||
name: "한맥가족",
|
||||
type: "COMPANY_GROUP",
|
||||
parentId: undefined,
|
||||
},
|
||||
{
|
||||
id: "external-id",
|
||||
slug: "external",
|
||||
name: "External",
|
||||
type: "COMPANY",
|
||||
parentId: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
expect(
|
||||
isHanmacFamilyUser(
|
||||
{
|
||||
companyCode: "external",
|
||||
tenant: tenants[1],
|
||||
metadata: { userType: "hanmac" },
|
||||
},
|
||||
tenants,
|
||||
"hanmac-family-id",
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,10 +5,13 @@ export type OrgChartTenantSelection = {
|
||||
|
||||
export type TenantFilterTarget = {
|
||||
id?: string;
|
||||
tenantId?: string;
|
||||
slug?: string;
|
||||
tenantSlug?: string;
|
||||
type?: string;
|
||||
parentId?: string | null;
|
||||
name?: string;
|
||||
tenantName?: string;
|
||||
};
|
||||
|
||||
export type HanmacFamilyUserTarget = {
|
||||
@@ -120,19 +123,43 @@ export function isHanmacFamilyUser<T extends TenantFilterTarget>(
|
||||
tenants: T[],
|
||||
hanmacFamilyTenantId?: string,
|
||||
) {
|
||||
const metadata = user.metadata ?? {};
|
||||
if (metadata.hanmacFamily === true || metadata.userType === "hanmac") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const metadataAppointments = Array.isArray(
|
||||
user.metadata?.additionalAppointments,
|
||||
)
|
||||
? user.metadata.additionalAppointments
|
||||
.map((appointment) => appointment as TenantFilterTarget)
|
||||
.filter(
|
||||
(appointment) =>
|
||||
typeof appointment.tenantId === "string" ||
|
||||
typeof appointment.id === "string" ||
|
||||
typeof appointment.tenantSlug === "string" ||
|
||||
typeof appointment.slug === "string",
|
||||
)
|
||||
.map((appointment) => ({
|
||||
id: appointment.id ?? appointment.tenantId,
|
||||
slug: appointment.slug ?? appointment.tenantSlug,
|
||||
parentId: appointment.parentId,
|
||||
type: appointment.type,
|
||||
name: appointment.name ?? appointment.tenantName,
|
||||
}))
|
||||
: [];
|
||||
const tenantBySlug = new Map(
|
||||
tenants
|
||||
.filter((tenant) => tenant.slug?.trim())
|
||||
.map((tenant) => [tenant.slug?.toLowerCase() as string, tenant]),
|
||||
);
|
||||
const tenantById = new Map(
|
||||
tenants
|
||||
.filter((tenant) => tenant.id?.trim())
|
||||
.map((tenant) => [tenant.id as string, tenant]),
|
||||
);
|
||||
const tenantCandidates = [
|
||||
user.tenant,
|
||||
...(user.joinedTenants ?? []),
|
||||
...metadataAppointments,
|
||||
...metadataAppointments.map((appointment) =>
|
||||
tenantById.get(appointment.id ?? ""),
|
||||
),
|
||||
tenantBySlug.get(user.companyCode?.toLowerCase() ?? ""),
|
||||
tenantBySlug.get(user.tenantSlug?.toLowerCase() ?? ""),
|
||||
];
|
||||
|
||||
37
adminfront/src/features/users/utils/personalTenant.test.ts
Normal file
37
adminfront/src/features/users/utils/personalTenant.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
GLOBAL_PERSONAL_TENANT_ID,
|
||||
resolvePersonalTenant,
|
||||
} from "./personalTenant";
|
||||
|
||||
describe("resolvePersonalTenant", () => {
|
||||
it("uses the fixed global Personal tenant when it is not included in the paged tenant list", () => {
|
||||
expect(resolvePersonalTenant([])).toMatchObject({
|
||||
id: GLOBAL_PERSONAL_TENANT_ID,
|
||||
slug: "personal",
|
||||
name: "Personal",
|
||||
type: "PERSONAL",
|
||||
status: "active",
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers the tenant returned by the API when available", () => {
|
||||
expect(
|
||||
resolvePersonalTenant([
|
||||
{
|
||||
id: "api-personal-id",
|
||||
slug: "personal",
|
||||
name: "Personal",
|
||||
type: "PERSONAL",
|
||||
status: "active",
|
||||
memberCount: 0,
|
||||
createdAt: "2026-05-01T00:00:00Z",
|
||||
updatedAt: "2026-05-01T00:00:00Z",
|
||||
},
|
||||
]),
|
||||
).toMatchObject({
|
||||
id: "api-personal-id",
|
||||
slug: "personal",
|
||||
});
|
||||
});
|
||||
});
|
||||
34
adminfront/src/features/users/utils/personalTenant.ts
Normal file
34
adminfront/src/features/users/utils/personalTenant.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { TenantSummary } from "../../../lib/adminApi";
|
||||
|
||||
export const GLOBAL_PERSONAL_TENANT_ID =
|
||||
import.meta.env.VITE_PERSONAL_TENANT_ID ||
|
||||
"9607eb7b-04d2-42ab-80fe-780fe21c7e8f";
|
||||
|
||||
export const GLOBAL_PERSONAL_TENANT_SLUG =
|
||||
import.meta.env.VITE_PERSONAL_TENANT_SLUG || "personal";
|
||||
|
||||
export function isPersonalTenant(
|
||||
tenant: Pick<TenantSummary, "name" | "slug" | "type">,
|
||||
) {
|
||||
return (
|
||||
tenant.slug === GLOBAL_PERSONAL_TENANT_SLUG ||
|
||||
(tenant.type === "PERSONAL" && tenant.name.toLowerCase() === "personal")
|
||||
);
|
||||
}
|
||||
|
||||
export function resolvePersonalTenant(tenants: TenantSummary[]): TenantSummary {
|
||||
const tenant = tenants.find(isPersonalTenant);
|
||||
if (tenant) return tenant;
|
||||
|
||||
return {
|
||||
id: GLOBAL_PERSONAL_TENANT_ID,
|
||||
slug: GLOBAL_PERSONAL_TENANT_SLUG,
|
||||
name: "Personal",
|
||||
type: "PERSONAL",
|
||||
description: "개인 사용자 기본 루트 테넌트",
|
||||
status: "active",
|
||||
memberCount: 0,
|
||||
createdAt: "2026-05-04T06:52:59.187802Z",
|
||||
updatedAt: "2026-05-04T06:52:59.191145Z",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user