diff --git a/adminfront/src/features/tenants/routes/TenantGroupsPage.tsx b/adminfront/src/features/tenants/routes/TenantGroupsPage.tsx index bc2c22fa..da214dc2 100644 --- a/adminfront/src/features/tenants/routes/TenantGroupsPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantGroupsPage.tsx @@ -2,13 +2,16 @@ import { type UseMutationResult, useMutation, useQuery, + useQueryClient, } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { + ArrowRightLeft, ChevronDown, ChevronRight, Plus, RefreshCw, + Search, Shield, Trash2, UserMinus, @@ -27,8 +30,18 @@ import { CardHeader, CardTitle, } from "../../../components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "../../../components/ui/dialog"; import { Input } from "../../../components/ui/input"; import { Label } from "../../../components/ui/label"; +import { ScrollArea } from "../../../components/ui/scroll-area"; import { Table, TableBody, @@ -44,6 +57,8 @@ import { createGroup, deleteGroup, fetchGroups, + fetchTenant, + fetchUsers, removeGroupMember, } from "../../../lib/adminApi"; import { t } from "../../../lib/i18n"; @@ -223,6 +238,7 @@ const UserGroupTreeNode: React.FC = ({ function TenantGroupsPage() { const params = useParams<{ tenantId: string }>(); const tenantId = params.tenantId ?? ""; + const queryClient = useQueryClient(); const [newGroupName, setNewGroupName] = useState(""); const [newGroupDesc, setNewGroupNameDesc] = useState(""); @@ -231,13 +247,35 @@ function TenantGroupsPage() { const [selectedGroupId, setSelectedGroupId] = useState(null); + // Modal States + const [isAddMemberModalOpen, setIsAddMemberModalOpen] = useState(false); + const [isMoveMemberModalOpen, setIsMoveMemberModalOpen] = useState(false); + const [memberActionTargetUserId, setMemberActionTargetUserId] = useState(null); + const [userSearchTerm, setUserSearchTerm] = useState(""); + const [groupSearchTerm, setGroupSearchTerm] = useState(""); + + // 테넌트 정보 조회 (slug 획득) + const tenantQuery = useQuery({ + queryKey: ["tenant", tenantId], + queryFn: () => fetchTenant(tenantId), + enabled: tenantId.length > 0, + }); + const tenantSlug = tenantQuery.data?.slug; + + // 해당 테넌트의 사용자 목록 조회 + const usersQuery = useQuery({ + queryKey: ["users", { tenantSlug }], + queryFn: () => fetchUsers(1000, 0, undefined, tenantSlug), + enabled: !!tenantSlug, + }); + const users = usersQuery.data?.items ?? []; + // 그룹 목록 조회 const groupsQuery = useQuery({ queryKey: ["groups", tenantId], queryFn: () => fetchGroups(tenantId), enabled: tenantId.length > 0, }); - // 그룹 생성 const createMutation = useMutation({ mutationFn: () => @@ -318,32 +356,48 @@ function TenantGroupsPage() { }, }); + // 멤버 이동 (Remove -> Add) + const moveMemberMutation = useMutation({ + mutationFn: async ({ + sourceGroupId, + targetGroupId, + userId, + }: { + sourceGroupId: string; + targetGroupId: string; + userId: string; + }) => { + await removeGroupMember(tenantId, sourceGroupId, userId); + await addGroupMember(tenantId, targetGroupId, userId); + }, + onSuccess: () => { + toast.success( + t("msg.admin.groups.members.move_success", "멤버가 이동되었습니다."), + ); + groupsQuery.refetch(); + setIsMoveMemberModalOpen(false); + setMemberActionTargetUserId(null); + }, + onError: (error: AxiosError<{ error?: string }>) => { + toast.error(t("msg.common.error", "오류 발생"), { + description: error.response?.data?.error || error.message, + }); + }, + }); + const groupTree = groupsQuery.data ? buildGroupTree(groupsQuery.data, tenantId) : []; const handleAddSubGroup = (parentId: string) => { setNewGroupParentId(parentId); - // Optionally scroll to the create form or highlight it }; - - const handleAddMember = (groupId: string) => { - const userId = window.prompt( - t( - "msg.admin.groups.prompt.user_id", - "추가할 사용자의 UUID를 입력하세요:", - ), - ); - if (userId) { - addMemberMutation.mutate({ groupId, userId }); - } - }; - const currentGroup = groupsQuery.data?.find((g) => g.id === selectedGroupId); return ( -
-
+ <> +
+
{/* 그룹 생성 폼 */} @@ -544,7 +598,7 @@ function TenantGroupsPage() {
+
+ + +
))} @@ -610,11 +689,205 @@ function TenantGroupsPage() {
- - - )} -
- ); -} + + + )} +
-export default TenantGroupsPage; + {/* Add Member Modal */} + { + setIsAddMemberModalOpen(val); + if (!val) setUserSearchTerm(""); + }} + > + + + + {t("ui.admin.groups.members.add_modal_title", "그룹에 멤버 추가")} + + + {t( + "msg.admin.groups.members.add_modal_desc", + "이 테넌트에 속한 사용자 중 추가할 멤버를 검색하여 선택하세요.", + )} + + +
+
+ + setUserSearchTerm(e.target.value)} + /> +
+ +
+ {usersQuery.isLoading ? ( +
+ {t("ui.common.loading", "로딩 중...")} +
+ ) : ( + users + .filter((u) => { + const term = userSearchTerm.toLowerCase(); + return ( + u.name.toLowerCase().includes(term) || + u.email.toLowerCase().includes(term) + ); + }) + .filter( + (u) => + !currentGroup?.members?.some((m) => m.id === u.id), + ) // Exclude existing members + .map((user) => ( +
+
+

{user.name}

+

+ {user.email} +

+
+ +
+ )) + )} + {users.length > 0 && + users.filter( + (u) => !currentGroup?.members?.some((m) => m.id === u.id), + ).length === 0 && ( +
+ {t("msg.admin.groups.members.all_added", "모든 테넌트 멤버가 이미 이 그룹에 속해 있습니다.")} +
+ )} +
+
+
+ + + +
+
+ + {/* Move Member Modal */} + { + setIsMoveMemberModalOpen(val); + if (!val) { + setMemberActionTargetUserId(null); + setGroupSearchTerm(""); + } + }} + > + + + + {t("ui.admin.groups.members.move_modal_title", "부서 이동")} + + + {t( + "msg.admin.groups.members.move_modal_desc", + "선택한 멤버를 이동할 대상 그룹을 선택하세요.", + )} + + +
+
+ + setGroupSearchTerm(e.target.value)} + /> +
+ +
+ {groupsQuery.isLoading ? ( +
+ {t("ui.common.loading", "로딩 중...")} +
+ ) : groupsQuery.data && groupsQuery.data.length > 0 ? ( + groupsQuery.data + .filter((g) => + g.name + .toLowerCase() + .includes(groupSearchTerm.toLowerCase()), + ) + .filter((g) => g.id !== currentGroup?.id) // Exclude current group + .map((group) => ( +
+
+ + {group.name} +
+ +
+ )) + ) : ( +
+ {t("msg.admin.groups.list.no_results", "그룹이 없습니다.")} +
+ )} +
+
+
+ + + +
+
+ + ); + } + + export default TenantGroupsPage; diff --git a/adminfront/src/features/tenants/routes/TenantListPage.tsx b/adminfront/src/features/tenants/routes/TenantListPage.tsx index 666ffb6f..e3459b49 100644 --- a/adminfront/src/features/tenants/routes/TenantListPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantListPage.tsx @@ -1,6 +1,9 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { + ArrowDown, + ArrowUp, + ArrowUpDown, Download, FileSpreadsheet, Pencil, @@ -50,22 +53,29 @@ import { importTenantsCSV, } from "../../../lib/adminApi"; import { t } from "../../../lib/i18n"; +import { type TenantNode, buildTenantFullTree } from "../../../lib/tenantTree"; +import { isSeedTenant } from "../utils/protectedTenants"; import { - type TenantImportResolution, type TenantImportPreviewRow, + type TenantImportResolution, buildTenantImportPreview, parseTenantCSV, serializeTenantImportCSV, } from "../utils/tenantCsvImport"; -import { isSeedTenant } from "../utils/protectedTenants"; const tenantCSVTemplate = "name,type,parent_tenant_slug,slug,memo,email_domain\n"; +type SortConfig = { + key: keyof TenantSummary | "recursiveMemberCount"; + direction: "asc" | "desc"; +}; + function TenantListPage() { const navigate = useNavigate(); const [selectedIds, setSelectedIds] = React.useState([]); const [search, setSearch] = React.useState(""); + const [sortConfig, setSortConfig] = React.useState(null); const fileInputRef = React.useRef(null); const [importMessage, setImportMessage] = React.useState(""); const [previewRows, setPreviewRows] = React.useState< @@ -198,14 +208,79 @@ function TenantListPage() { const allTenants = query.data?.items ?? []; const tenants = React.useMemo(() => { - if (!search.trim()) return allTenants; - const term = search.toLowerCase(); - return allTenants.filter( - (t) => - t.name.toLowerCase().includes(term) || - t.slug.toLowerCase().includes(term), + // 1. Calculate recursive counts + // buildTenantFullTree returns subTree which represents roots, but it also mutates the mapped nodes internally. + // However, to easily map them back to a flat list, we can just run the builder, + // and then extract the recursive counts. + const treeResult = buildTenantFullTree(allTenants); + + // Flatten the tree or just extract from allTenants map? + // buildTenantFullTree does NOT mutate the objects passed in allTenants. It creates new ones. + // Let's create a map of id -> recursiveMemberCount + const recursiveCounts = new Map(); + const extractCounts = (nodes: TenantNode[]) => { + for (const node of nodes) { + recursiveCounts.set(node.id, node.recursiveMemberCount); + if (node.children) extractCounts(node.children); + } + }; + extractCounts(treeResult.subTree); + + let enriched = allTenants.map((t) => ({ + ...t, + recursiveMemberCount: recursiveCounts.get(t.id) ?? t.memberCount ?? 0, + })); + + if (search.trim()) { + const term = search.toLowerCase(); + enriched = enriched.filter( + (t) => + t.name.toLowerCase().includes(term) || + t.slug.toLowerCase().includes(term), + ); + } + + if (sortConfig) { + enriched.sort((a, b) => { + const aValue = a[sortConfig.key as keyof typeof a]; + const bValue = b[sortConfig.key as keyof typeof b]; + + if (aValue === bValue) return 0; + if (aValue === null || aValue === undefined) return 1; + if (bValue === null || bValue === undefined) return -1; + + if (sortConfig.direction === "asc") { + return aValue < bValue ? -1 : 1; + } + return aValue > bValue ? -1 : 1; + }); + } + + return enriched; + }, [allTenants, search, sortConfig]); + + const requestSort = (key: SortConfig["key"]) => { + let direction: "asc" | "desc" = "asc"; + if ( + sortConfig && + sortConfig.key === key && + sortConfig.direction === "asc" + ) { + direction = "desc"; + } + setSortConfig({ key, direction }); + }; + + const getSortIcon = (key: SortConfig["key"]) => { + if (!sortConfig || sortConfig.key !== key) { + return ; + } + return sortConfig.direction === "asc" ? ( + + ) : ( + ); - }, [allTenants, search]); + }; const deletableTenants = React.useMemo( () => tenants.filter((tenant) => !isSeedTenant(tenant)), @@ -358,6 +433,19 @@ function TenantListPage() {

+
+ + setSearch(e.target.value)} + /> +
+ {selectedIds.length > 0 && ( + +
@@ -69,13 +112,25 @@ function TenantUsersPage() { {t("ui.admin.tenants.members.table.status", "STATUS")} + + {t("ui.admin.tenants.members.table.actions", "ACTIONS")} + - {users.length === 0 && ( + {usersQuery.isLoading ? ( + + +
+ + {t("ui.common.loading", "Loading...")} +
+
+
+ ) : users.length === 0 ? ( {t( @@ -84,33 +139,59 @@ function TenantUsersPage() { )} + ) : ( + users.map((user) => ( + + {user.name} + +
+ + {user.email} +
+
+ + + {t( + `ui.common.role.${user.role}`, + user.role.replace("_", " "), + )} + + + + + {t(`ui.common.status.${user.status}`, user.status)} + + + + + + + + + + + + {t("ui.admin.tenants.members.view_profile", "상세 정보")} + + + handleRemoveMember(user.id, user.name)} + disabled={removeTenantMutation.isPending} + > + + {t("ui.admin.tenants.members.remove", "조직에서 제외")} + + + + +
+ )) )} - {users.map((user) => ( - - {user.name} - -
- - {user.email} -
-
- - - {t( - `ui.common.role.${user.role}`, - user.role.replace("_", " "), - )} - - - - - {t(`ui.common.status.${user.status}`, user.status)} - - -
- ))}
diff --git a/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx b/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx index 42f88448..f6aa4060 100644 --- a/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx +++ b/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx @@ -195,7 +195,9 @@ const SidebarNode: React.FC<{ const MemberTable: React.FC<{ tenantSlug: string; onRefreshTrigger?: number; -}> = ({ tenantSlug, onRefreshTrigger }) => { + allTenants?: TenantSummary[]; +}> = ({ tenantSlug, onRefreshTrigger, allTenants }) => { + const queryClient = useQueryClient(); const { data, isLoading, refetch } = useQuery({ queryKey: ["tenant-members-v2", tenantSlug, onRefreshTrigger], queryFn: () => fetchUsers(100, 0, undefined, tenantSlug), @@ -204,6 +206,54 @@ const MemberTable: React.FC<{ const members = data?.items ?? []; + const [isMoveOpen, setIsMoveOpen] = useState(false); + const [selectedUser, setSelectedUser] = useState(null); + const [targetTenantSlug, setTargetTenantSlug] = useState(""); + const [searchTenant, setSearchTenant] = useState(""); + + const moveMutation = useMutation({ + mutationFn: (newSlug: string) => { + if (!selectedUser) throw new Error("No user selected"); + return updateUser(selectedUser.id, { tenantSlug: newSlug }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["tenants-full-tree-v2"] }); + toast.success( + t("msg.info.saved_success", "사용자 조직이 변경되었습니다."), + ); + setIsMoveOpen(false); + setSelectedUser(null); + refetch(); + }, + onError: () => toast.error(t("msg.common.error", "오류가 발생했습니다.")), + }); + + const removeMutation = useMutation({ + mutationFn: (userId: string) => updateUser(userId, { tenantSlug: "" }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["tenants-full-tree-v2"] }); + toast.success(t("msg.info.saved_success", "조직에서 제외되었습니다.")); + refetch(); + }, + onError: () => toast.error(t("msg.common.error", "오류가 발생했습니다.")), + }); + + const handleMoveClick = (user: UserSummary) => { + setSelectedUser(user); + setTargetTenantSlug(""); + setIsMoveOpen(true); + }; + + const filteredTenants = React.useMemo(() => { + if (!allTenants) return []; + if (!searchTenant) return allTenants; + return allTenants.filter( + (t) => + t.name.toLowerCase().includes(searchTenant.toLowerCase()) || + t.slug.toLowerCase().includes(searchTenant.toLowerCase()), + ); + }, [allTenants, searchTenant]); + if (isLoading) return (
@@ -264,6 +314,28 @@ const MemberTable: React.FC<{ {t("ui.common.detail", "상세보기")} + + handleMoveClick(user)}> + + {t("ui.common.move_org", "타 조직으로 이동")} + + { + if ( + window.confirm( + t( + "msg.admin.users.confirm_remove_org", + "이 조직에서 사용자를 제외하시겠습니까?", + ), + ) + ) { + removeMutation.mutate(user.id); + } + }} + > + + {t("ui.common.remove_org", "조직에서 제외")} + @@ -271,6 +343,65 @@ const MemberTable: React.FC<{ ))} + + + + + + {t("ui.common.move_org", "타 조직으로 이동")} + + + {selectedUser?.name} 사용자를 이동할 타 조직을 선택하세요. + + +
+ setSearchTenant(e.target.value)} + /> + +
+ {filteredTenants.map((tItem) => ( + + ))} + {filteredTenants.length === 0 && ( +
+ {t("msg.common.no_results", "검색 결과가 없습니다.")} +
+ )} +
+
+
+ + + + +
+
); }; @@ -574,6 +705,7 @@ function TenantUserGroupsTab() { @@ -702,6 +834,7 @@ const UserAddDialog: React.FC<{ setIsSubmitting(true); try { await updateUser(selectedUserId, { tenantSlug }); + queryClient.invalidateQueries({ queryKey: ["tenants-full-tree-v2"] }); toast.success(t("msg.info.saved_success", "사용자가 배정되었습니다.")); onOpenChange(false); resetFields(); diff --git a/adminfront/src/features/users/UserCreatePage.tsx b/adminfront/src/features/users/UserCreatePage.tsx index 5900db79..8f2e1725 100644 --- a/adminfront/src/features/users/UserCreatePage.tsx +++ b/adminfront/src/features/users/UserCreatePage.tsx @@ -11,7 +11,7 @@ import { } from "lucide-react"; import * as React from "react"; import { useForm } from "react-hook-form"; -import { Link, useNavigate } from "react-router-dom"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; import { Button } from "../../components/ui/button"; import { Card, @@ -104,6 +104,7 @@ function createEmptyAppointment(): AppointmentDraft { function UserCreatePage() { const navigate = useNavigate(); + const [searchParams] = useSearchParams(); const queryClient = useQueryClient(); const [error, setError] = React.useState(null); const [generatedPassword, setGeneratedPassword] = React.useState< @@ -144,7 +145,7 @@ function UserCreatePage() { password: "", name: "", phone: "", - tenantSlug: "", + tenantSlug: searchParams.get("tenantSlug") || "", department: "", position: "", jobTitle: "", diff --git a/adminfront/src/features/users/UserDetailPage.tsx b/adminfront/src/features/users/UserDetailPage.tsx index 791b5f33..958685cd 100644 --- a/adminfront/src/features/users/UserDetailPage.tsx +++ b/adminfront/src/features/users/UserDetailPage.tsx @@ -44,13 +44,6 @@ 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, @@ -85,7 +78,6 @@ import { parseOrgChartTenantSelection, } from "./orgChartPicker"; import type { UserSchemaField } from "./userSchemaFields"; -import { userStatusLabel, userStatusValues } from "./userStatus"; type UserFormValues = Omit & { metadata: Record>; @@ -131,40 +123,12 @@ function createEmptyAppointment(): AppointmentDraft { tenantId: "", tenantName: "", tenantSlug: "", - isPrimary: false, isOwner: false, jobTitle: "", position: "", }; } -function normalizePrimaryAppointments( - appointments: AppointmentDraft[], -): AppointmentDraft[] { - const leafIndexes = appointments - .map((appointment, index) => - appointment.tenantId.trim().length > 0 ? index : -1, - ) - .filter((index) => index >= 0); - if (leafIndexes.length === 1) { - const primaryIndex = leafIndexes[0]; - return appointments.map((appointment, index) => ({ - ...appointment, - isPrimary: index === primaryIndex, - })); - } - const selectedIndex = appointments.findIndex( - (appointment) => appointment.isPrimary === true, - ); - return appointments.map((appointment, index) => ({ - ...appointment, - isPrimary: - selectedIndex >= 0 && - index === selectedIndex && - appointment.tenantId.trim().length > 0, - })); -} - function validateManualPassword( password: string, policy?: PasswordPolicyResponse, @@ -521,17 +485,15 @@ function UserDetailPage() { try { const tenant = await resolveTenantSelection(selection, tenants); setAdditionalAppointments((current) => - normalizePrimaryAppointments( - current.map((appointment, index) => - index === target.index - ? { - ...appointment, - tenantId: tenant.id, - tenantName: tenant.name, - tenantSlug: tenant.slug, - } - : appointment, - ), + current.map((appointment, index) => + index === target.index + ? { + ...appointment, + tenantId: tenant.id, + tenantName: tenant.name, + tenantSlug: tenant.slug, + } + : appointment, ), ); setPickerTarget(null); @@ -574,30 +536,15 @@ function UserDetailPage() { patch: Partial, ) => { setAdditionalAppointments((current) => - normalizePrimaryAppointments( - current.map((appointment, currentIndex) => - currentIndex === index ? { ...appointment, ...patch } : appointment, - ), - ), - ); - }; - - const setPrimaryAppointment = (index: number, checked: boolean) => { - setAdditionalAppointments((current) => - normalizePrimaryAppointments( - current.map((appointment, currentIndex) => ({ - ...appointment, - isPrimary: checked && currentIndex === index, - })), + current.map((appointment, currentIndex) => + currentIndex === index ? { ...appointment, ...patch } : appointment, ), ); }; const removeAppointment = (index: number) => { setAdditionalAppointments((current) => - normalizePrimaryAppointments( - current.filter((_, currentIndex) => currentIndex !== index), - ), + current.filter((_, currentIndex) => currentIndex !== index), ); }; @@ -655,10 +602,7 @@ function UserDetailPage() { tenantSlug: user.companyCode || user.joinedTenants?.find( - (t) => - t.type === "COMPANY" || - t.type === "COMPANY_GROUP" || - t.type === "ORGANIZATION", + (t) => t.type === "COMPANY" || t.type === "COMPANY_GROUP", )?.slug || "", department: user.department || "", @@ -692,45 +636,38 @@ function UserDetailPage() { isHanmacFamilyTenant(tenant, tenants, hanmacFamilyTenantId), ); setAdditionalAppointments( - normalizePrimaryAppointments( - Array.isArray(rawAppointments) - ? (rawAppointments as UserAppointment[]).map((appointment) => ({ - ...appointment, - isPrimary: - appointment.isPrimary === true || - appointment.tenantId === metadata.primaryTenantId, - draftId: createDraftId(), - })) - : isUserHanmacFamily - ? familyFallbackTenants.length > 0 - ? familyFallbackTenants.map((tenant) => ({ - draftId: createDraftId(), - tenantId: tenant.id, - tenantName: tenant.name, - tenantSlug: tenant.slug, - isPrimary: tenant.id === fallbackAppointment?.id, - isOwner: - metadata.primaryTenantIsOwner === true && - tenant.id === fallbackAppointment?.id, - jobTitle: user.jobTitle, - position: user.position, - })) - : fallbackAppointment - ? [ - { - draftId: createDraftId(), - tenantId: fallbackAppointment.id, - tenantName: fallbackAppointment.name, - tenantSlug: fallbackAppointment.slug, - isPrimary: true, - isOwner: metadata.primaryTenantIsOwner === true, - jobTitle: user.jobTitle, - position: user.position, - }, - ] - : [] - : [], - ), + Array.isArray(rawAppointments) + ? (rawAppointments as UserAppointment[]).map((appointment) => ({ + ...appointment, + draftId: createDraftId(), + })) + : isUserHanmacFamily + ? familyFallbackTenants.length > 0 + ? familyFallbackTenants.map((tenant) => ({ + draftId: createDraftId(), + tenantId: tenant.id, + tenantName: tenant.name, + tenantSlug: tenant.slug, + isOwner: + metadata.primaryTenantIsOwner === true && + tenant.id === fallbackAppointment?.id, + jobTitle: user.jobTitle, + position: user.position, + })) + : fallbackAppointment + ? [ + { + draftId: createDraftId(), + tenantId: fallbackAppointment.id, + tenantName: fallbackAppointment.name, + tenantSlug: fallbackAppointment.slug, + isOwner: metadata.primaryTenantIsOwner === true, + jobTitle: user.jobTitle, + position: user.position, + }, + ] + : [] + : [], ); } }, [hanmacFamilyTenantId, tenants, user, reset]); @@ -811,37 +748,19 @@ function UserDetailPage() { tenantId: appointment.tenantId, tenantSlug: appointment.tenantSlug, tenantName: appointment.tenantName, - isPrimary: appointment.isPrimary === true, isOwner: appointment.isOwner, jobTitle: appointment.jobTitle, position: appointment.position, })); - const primaryAppointment = appointments.find( - (appointment) => appointment.isPrimary, - ); payload.tenantSlug = undefined; payload.department = undefined; payload.position = undefined; payload.jobTitle = undefined; payload.additionalAppointments = appointments; - if (primaryAppointment) { - payload.tenantSlug = primaryAppointment.tenantSlug; - payload.primaryTenantId = primaryAppointment.tenantId; - payload.primaryTenantName = primaryAppointment.tenantName; - payload.primaryTenantIsOwner = primaryAppointment.isOwner; - } payload.metadata = { ...metadata, additionalAppointments: appointments, - ...(primaryAppointment - ? { - primaryTenantId: primaryAppointment.tenantId, - primaryTenantName: primaryAppointment.tenantName, - primaryTenantSlug: primaryAppointment.tenantSlug, - primaryTenantIsOwner: primaryAppointment.isOwner, - } - : {}), }; } @@ -872,9 +791,6 @@ function UserDetailPage() { filterNonHanmacFamilyTenants(userAffiliatedTenants, hanmacFamilyTenantId), [userAffiliatedTenants, hanmacFamilyTenantId], ); - const primaryAppointmentLeafCount = additionalAppointments.filter( - (appointment) => appointment.tenantId.trim().length > 0, - ).length; if (isLoading) { return ( @@ -941,10 +857,7 @@ function UserDetailPage() { {user.tenant?.name || user.companyCode || user.joinedTenants?.find( - (t) => - t.type === "COMPANY" || - t.type === "COMPANY_GROUP" || - t.type === "ORGANIZATION", + (t) => t.type === "COMPANY" || t.type === "COMPANY_GROUP", )?.name || t("ui.admin.users.detail.form.tenant_global", "시스템 전역")} @@ -1088,23 +1001,21 @@ function UserDetailPage() { > {t("ui.admin.users.detail.form.status", "상태")} - +
+ + setValue("status", checked ? "active" : "inactive") + } + /> + + {t( + `ui.common.status.${watchedStatus}`, + watchedStatus || "inactive", + )} + +
@@ -1249,26 +1160,6 @@ function UserDetailPage() { {appointment.tenantSlug} )} -