1
0
forked from baron/baron-sso

사용자 상태 세분화

This commit is contained in:
2026-05-20 10:17:15 +09:00
parent 9112c4fb36
commit 42b49674cc
33 changed files with 876 additions and 590 deletions

View File

@@ -44,6 +44,13 @@ import {
} from "../../components/ui/dialog";
import { Input } from "../../components/ui/input";
import { Label } from "../../components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../components/ui/select";
import { Switch } from "../../components/ui/switch";
import {
Tabs,
@@ -78,6 +85,11 @@ import {
parseOrgChartTenantSelection,
} from "./orgChartPicker";
import type { UserSchemaField } from "./userSchemaFields";
import {
normalizeUserStatusValue,
userStatusLabel,
userStatusValues,
} from "./userStatus";
import { resolvePersonalTenant } from "./utils/personalTenant";
type UserFormValues = Omit<UserUpdateRequest, "metadata"> & {
@@ -603,7 +615,7 @@ function UserDetailPage() {
name: user.name,
phone: user.phone || "",
role: user.role,
status: user.status,
status: normalizeUserStatusValue(user.status),
tenantSlug:
user.tenantSlug ||
user.joinedTenants?.find(
@@ -1044,21 +1056,25 @@ function UserDetailPage() {
>
{t("ui.admin.users.detail.form.status", "상태")}
</Label>
<div className="flex h-11 items-center gap-3 rounded-md border border-input bg-background px-3">
<Switch
id="status"
checked={watchedStatus === "active"}
onCheckedChange={(checked) =>
setValue("status", checked ? "active" : "inactive")
}
/>
<span className="text-sm text-muted-foreground">
{t(
`ui.common.status.${watchedStatus}`,
watchedStatus || "inactive",
)}
</span>
</div>
<Select
value={normalizeUserStatusValue(watchedStatus || "")}
onValueChange={(value) =>
setValue("status", normalizeUserStatusValue(value), {
shouldDirty: true,
})
}
>
<SelectTrigger id="status" className="h-11 shadow-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
{userStatusValues.map((status) => (
<SelectItem key={status} value={status}>
{userStatusLabel(status)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>

View File

@@ -67,7 +67,6 @@ import {
SelectTrigger,
SelectValue,
} from "../../components/ui/select";
import { Switch } from "../../components/ui/switch";
import {
Table,
TableBody,
@@ -93,6 +92,7 @@ import { t } from "../../lib/i18n";
import { isSuperAdminRole } from "../../lib/roles";
import { UserBulkUploadModal } from "./components/UserBulkUploadModal";
import {
normalizeUserStatusValue,
type UserStatusValue,
userStatusLabel,
userStatusValues,
@@ -776,30 +776,37 @@ function UserListPage() {
{user.id}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Switch
checked={user.status === "active"}
onCheckedChange={(checked) =>
statusMutation.mutate({
userId: user.id,
status: checked ? "active" : "inactive",
})
}
disabled={
statusMutation.isPending ||
user.id === profile?.id
}
<Select
value={normalizeUserStatusValue(user.status)}
onValueChange={(status) =>
statusMutation.mutate({
userId: user.id,
status,
})
}
disabled={
statusMutation.isPending || user.id === profile?.id
}
>
<SelectTrigger
className="h-8 w-[150px] border-none bg-transparent hover:bg-muted/50 transition-colors px-0 font-medium"
aria-label={t(
"ui.admin.users.list.toggle_status",
"{{name}} 활성 상태",
"ui.admin.users.list.change_status",
"{{name}} 상태 변경",
{ name: user.name },
)}
data-testid={`user-status-toggle-${user.id}`}
/>
<span className="text-sm text-muted-foreground">
{t(`ui.common.status.${user.status}`, user.status)}
</span>
</div>
data-testid={`user-status-select-${user.id}`}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
{userStatusValues.map((status) => (
<SelectItem key={status} value={status}>
{userStatusLabel(status)}
</SelectItem>
))}
</SelectContent>
</Select>
</TableCell>
<TableCell>
<Select
@@ -894,13 +901,11 @@ function UserListPage() {
/>
</SelectTrigger>
<SelectContent>
{userStatusValues
.filter((s) => s === "active" || s === "inactive")
.map((status) => (
<SelectItem key={status} value={status}>
{userStatusLabel(status)}
</SelectItem>
))}
{userStatusValues.map((status) => (
<SelectItem key={status} value={status}>
{userStatusLabel(status)}
</SelectItem>
))}
</SelectContent>
</Select>
{canPromoteSuperAdmin && (

View File

@@ -0,0 +1,38 @@
// @vitest-environment node
import { describe, expect, it, vi } from "vitest";
import {
normalizeUserStatusValue,
userStatusLabel,
userStatusValues,
} from "./userStatus";
vi.mock("../../lib/i18n", () => ({
t: (key: string, fallback?: string) => fallback ?? key,
}));
describe("userStatus", () => {
it("exposes canonical user status values", () => {
expect(userStatusValues).toEqual([
"active",
"temporary_leave",
"suspended",
"preboarding",
"baron_guest",
"extended_leave",
"archived",
]);
});
it("normalizes legacy status values", () => {
expect(normalizeUserStatusValue("inactive")).toBe("preboarding");
expect(normalizeUserStatusValue("leave_of_absence")).toBe(
"temporary_leave",
);
expect(normalizeUserStatusValue("baron_only")).toBe("baron_guest");
});
it("uses canonical labels for legacy status values", () => {
expect(userStatusLabel("baron_only")).toBe("baron_guest");
});
});

View File

@@ -2,13 +2,42 @@ import { t } from "../../lib/i18n";
export const userStatusValues = [
"active",
"inactive",
"temporary_leave",
"suspended",
"leave_of_absence",
"preboarding",
"baron_guest",
"extended_leave",
"archived",
] as const;
export type UserStatusValue = (typeof userStatusValues)[number];
export function userStatusLabel(status: string) {
return t(`ui.common.status.${status}`, status);
export function normalizeUserStatusValue(status: string): UserStatusValue {
switch (status.trim().toLowerCase()) {
case "active":
return "active";
case "temporary_leave":
case "leave_of_absence":
return "temporary_leave";
case "suspended":
case "blocked":
return "suspended";
case "preboarding":
case "inactive":
return "preboarding";
case "baron_guest":
case "baron_only":
return "baron_guest";
case "extended_leave":
return "extended_leave";
case "archived":
return "archived";
default:
return "preboarding";
}
}
export function userStatusLabel(status: string) {
const normalized = normalizeUserStatusValue(status);
return t(`ui.common.status.${normalized}`, normalized);
}