forked from baron/baron-sso
chore: snapshot local state before dev merge
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -104,6 +104,10 @@ import {
|
||||
type TenantImportPreviewRow,
|
||||
type TenantImportResolution,
|
||||
} from "../utils/tenantCsvImport";
|
||||
import {
|
||||
TENANT_VISIBILITY_OPTIONS,
|
||||
type TenantVisibility,
|
||||
} from "../utils/orgConfig";
|
||||
import {
|
||||
filterTenantsByScope,
|
||||
filterTenantViewRowsBySearch,
|
||||
@@ -119,14 +123,30 @@ const tenantCSVTemplate =
|
||||
const tenantPageSize = 500;
|
||||
const _tenantVirtualizationThreshold = 250;
|
||||
const _tenantEstimatedRowHeight = 73;
|
||||
type TenantSortKey = keyof TenantSummary | "recursiveMemberCount";
|
||||
|
||||
const tenantTableHeadClassName =
|
||||
"h-9 px-3 py-1 text-xs leading-tight align-middle whitespace-nowrap";
|
||||
const tenantTableHeadInteractiveClassName = `${tenantTableHeadClassName} cursor-pointer transition-colors hover:bg-muted/50`;
|
||||
const tenantTableHeadContentClassName = "flex h-full items-center gap-1";
|
||||
const _tenantLoadAheadPx = 360;
|
||||
const _tenantLoadAheadRows = 30;
|
||||
|
||||
type TenantSortKey = keyof TenantSummary | "recursiveMemberCount";
|
||||
const backendTenantSortKeys = new Set<TenantSortKey>([
|
||||
"createdAt",
|
||||
"id",
|
||||
"name",
|
||||
"slug",
|
||||
"status",
|
||||
"type",
|
||||
"updatedAt",
|
||||
]);
|
||||
const bulkTenantTypeOptions = [
|
||||
{ value: "COMPANY", label: "COMPANY (일반 기업)" },
|
||||
{ value: "COMPANY_GROUP", label: "COMPANY_GROUP (그룹사/지주사)" },
|
||||
{ value: "ORGANIZATION", label: "ORGANIZATION (정규 조직)" },
|
||||
{ value: "USER_GROUP", label: "USER_GROUP (내부 부서/팀)" },
|
||||
{ value: "PERSONAL", label: "PERSONAL (개인 워크스페이스)" },
|
||||
] as const;
|
||||
|
||||
const getTenantIcon = (type?: string) => {
|
||||
switch (type?.toUpperCase()) {
|
||||
@@ -370,6 +390,10 @@ function TenantListPage() {
|
||||
const [search, setSearch] = React.useState("");
|
||||
const debouncedSearch = React.useDeferredValue(search.trim());
|
||||
const [selectedBulkStatus, setSelectedBulkStatus] = React.useState("");
|
||||
const [selectedBulkType, setSelectedBulkType] = React.useState("");
|
||||
const [selectedBulkVisibility, setSelectedBulkVisibility] = React.useState<
|
||||
TenantVisibility | ""
|
||||
>("");
|
||||
const _tenantTableScrollRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { data: profile } = useQuery({
|
||||
@@ -380,9 +404,20 @@ function TenantListPage() {
|
||||
const isWritable =
|
||||
profileRole === "super_admin" ||
|
||||
!!profile?.systemPermissions?.manage_tenants;
|
||||
const backendSortKey =
|
||||
sortConfig && backendTenantSortKeys.has(sortConfig.key)
|
||||
? sortConfig.key
|
||||
: undefined;
|
||||
|
||||
const query = useInfiniteQuery({
|
||||
queryKey: ["tenants", "lazy", debouncedSearch, scopeTenantId],
|
||||
queryKey: [
|
||||
"tenants",
|
||||
"lazy",
|
||||
debouncedSearch,
|
||||
scopeTenantId,
|
||||
backendSortKey,
|
||||
sortConfig?.direction,
|
||||
],
|
||||
queryFn: ({ pageParam }) =>
|
||||
fetchTenants(
|
||||
tenantPageSize,
|
||||
@@ -390,12 +425,19 @@ function TenantListPage() {
|
||||
scopeTenantId || undefined,
|
||||
pageParam ? (pageParam as string) : undefined,
|
||||
debouncedSearch,
|
||||
backendSortKey,
|
||||
sortConfig?.direction,
|
||||
),
|
||||
initialPageParam: "",
|
||||
getNextPageParam: (lastPage) =>
|
||||
lastPage.nextCursor || lastPage.next_cursor || undefined,
|
||||
});
|
||||
|
||||
const rawTenants = React.useMemo(
|
||||
() => query.data?.pages.flatMap((page) => page.items) ?? [],
|
||||
[query.data?.pages],
|
||||
);
|
||||
|
||||
const deleteBulkMutation = useMutation({
|
||||
mutationFn: (ids: string[]) => deleteTenantsBulk(ids),
|
||||
onSuccess: () => {
|
||||
@@ -404,21 +446,37 @@ function TenantListPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const bulkUpdateStatusMutation = useMutation({
|
||||
const bulkUpdateTenantsMutation = useMutation({
|
||||
mutationFn: async ({
|
||||
tenantIds,
|
||||
status,
|
||||
type,
|
||||
visibility,
|
||||
}: {
|
||||
tenantIds: string[];
|
||||
status: string;
|
||||
status?: string;
|
||||
type?: string;
|
||||
visibility?: TenantVisibility;
|
||||
}) => {
|
||||
// Execute sequential updates to avoid rate limits or partial failures
|
||||
await Promise.all(tenantIds.map((id) => updateTenant(id, { status })));
|
||||
await Promise.all(
|
||||
tenantIds.map((id) => {
|
||||
const source = rawTenants.find((tenant) => tenant.id === id);
|
||||
return updateTenant(id, {
|
||||
...(status ? { status } : {}),
|
||||
...(type ? { type } : {}),
|
||||
...(visibility
|
||||
? { config: { ...(source?.config ?? {}), visibility } }
|
||||
: {}),
|
||||
});
|
||||
}),
|
||||
);
|
||||
},
|
||||
onSuccess: () => {
|
||||
query.refetch();
|
||||
setSelectedIds([]);
|
||||
setSelectedBulkStatus("");
|
||||
setSelectedBulkType("");
|
||||
setSelectedBulkVisibility("");
|
||||
toast.success(
|
||||
t(
|
||||
"msg.admin.tenants.bulk.update_success",
|
||||
@@ -437,10 +495,19 @@ function TenantListPage() {
|
||||
});
|
||||
|
||||
const handleApplyBulkStatus = () => {
|
||||
if (selectedIds.length === 0 || !selectedBulkStatus) return;
|
||||
bulkUpdateStatusMutation.mutate({
|
||||
if (
|
||||
selectedIds.length === 0 ||
|
||||
(!selectedBulkStatus && !selectedBulkType && !selectedBulkVisibility)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
bulkUpdateTenantsMutation.mutate({
|
||||
tenantIds: selectedIds,
|
||||
status: selectedBulkStatus,
|
||||
...(selectedBulkStatus ? { status: selectedBulkStatus } : {}),
|
||||
...(selectedBulkType ? { type: selectedBulkType } : {}),
|
||||
...(selectedBulkVisibility
|
||||
? { visibility: selectedBulkVisibility }
|
||||
: {}),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -491,11 +558,6 @@ function TenantListPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const rawTenants = React.useMemo(
|
||||
() => query.data?.pages.flatMap((page) => page.items) ?? [],
|
||||
[query.data?.pages],
|
||||
);
|
||||
|
||||
const errorMsg = (query.error as AxiosError<{ error?: string }>)?.response
|
||||
?.data?.error;
|
||||
const fallbackError =
|
||||
@@ -1067,15 +1129,66 @@ function TenantListPage() {
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={selectedBulkType} onValueChange={setSelectedBulkType}>
|
||||
<SelectTrigger
|
||||
className="h-8 w-[180px] bg-transparent border-background/20 text-background text-xs"
|
||||
data-testid="tenant-bulk-type-select"
|
||||
>
|
||||
<SelectValue
|
||||
placeholder={t(
|
||||
"ui.admin.tenants.bulk.type_placeholder",
|
||||
"유형 선택",
|
||||
)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{bulkTenantTypeOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{t(
|
||||
`domain.tenant_type.${option.value.toLowerCase()}`,
|
||||
option.label,
|
||||
)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={selectedBulkVisibility}
|
||||
onValueChange={(value) =>
|
||||
setSelectedBulkVisibility(value as TenantVisibility)
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="h-8 w-[130px] bg-transparent border-background/20 text-background text-xs"
|
||||
data-testid="tenant-bulk-visibility-select"
|
||||
>
|
||||
<SelectValue
|
||||
placeholder={t(
|
||||
"ui.admin.tenants.bulk.visibility_placeholder",
|
||||
"공개 범위",
|
||||
)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TENANT_VISIBILITY_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-background hover:bg-background/10 h-8"
|
||||
onClick={handleApplyBulkStatus}
|
||||
disabled={
|
||||
!selectedBulkStatus || bulkUpdateStatusMutation.isPending
|
||||
(!selectedBulkStatus &&
|
||||
!selectedBulkType &&
|
||||
!selectedBulkVisibility) ||
|
||||
bulkUpdateTenantsMutation.isPending
|
||||
}
|
||||
data-testid="tenant-bulk-apply-status-btn"
|
||||
data-testid="tenant-bulk-apply-btn"
|
||||
>
|
||||
{t("ui.common.apply", "적용")}
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import type React from "react";
|
||||
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createI18nMock } from "../../../test/i18nMock";
|
||||
import { TenantProfilePage } from "./TenantProfilePage";
|
||||
|
||||
const fetchAllTenantsMock = vi.hoisted(() => vi.fn());
|
||||
const fetchTenantMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../../../lib/i18n", () => createI18nMock());
|
||||
|
||||
vi.mock("../../../lib/adminApi", () => ({
|
||||
approveTenant: vi.fn(),
|
||||
deleteTenant: vi.fn(),
|
||||
fetchAllTenants: fetchAllTenantsMock,
|
||||
fetchMe: vi.fn(async () => ({
|
||||
id: "admin-1",
|
||||
role: "super_admin",
|
||||
})),
|
||||
fetchTenant: fetchTenantMock,
|
||||
updateTenant: vi.fn(),
|
||||
}));
|
||||
|
||||
function renderWithProviders(ui: React.ReactElement) {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter initialEntries={["/tenants/tenant-leaf"]}>
|
||||
{ui}
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("TenantProfilePage initial profile loading", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
fetchTenantMock.mockResolvedValue({
|
||||
id: "tenant-leaf",
|
||||
type: "USER_GROUP",
|
||||
parentId: "tenant-company",
|
||||
name: "기술기획",
|
||||
slug: "tech-planning",
|
||||
description: "",
|
||||
status: "active",
|
||||
domains: [],
|
||||
memberCount: 0,
|
||||
config: {
|
||||
orgUnitType: "팀",
|
||||
visibility: "internal",
|
||||
worksmobileExcluded: true,
|
||||
},
|
||||
createdAt: "2026-06-17T00:00:00Z",
|
||||
updatedAt: "2026-06-17T00:00:00Z",
|
||||
});
|
||||
});
|
||||
|
||||
it("renders tenant config fields from the tenant response before the full tenant list resolves", async () => {
|
||||
fetchAllTenantsMock.mockReturnValue(new Promise(() => undefined));
|
||||
|
||||
renderWithProviders(
|
||||
<Routes>
|
||||
<Route path="/tenants/:tenantId" element={<TenantProfilePage />} />
|
||||
</Routes>,
|
||||
);
|
||||
|
||||
expect(await screen.findByDisplayValue("기술기획")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tenant-org-unit-type-select")).toHaveValue("팀");
|
||||
expect(screen.getByLabelText("공개 범위")).toHaveValue("internal");
|
||||
expect(screen.getByLabelText("WORKS 연동")).toHaveValue("excluded");
|
||||
expect(fetchAllTenantsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -57,11 +57,6 @@ export function TenantProfilePage() {
|
||||
const isWritable = hasPermission("manage_profile") || hasPermission("manage");
|
||||
const canView = hasPermission("view_profile") || hasPermission("view");
|
||||
|
||||
const parentQuery = useQuery({
|
||||
queryKey: ["tenants", "list-all"],
|
||||
queryFn: () => fetchAllTenants(),
|
||||
});
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [type, setType] = useState("COMPANY");
|
||||
const [slug, setSlug] = useState("");
|
||||
@@ -94,6 +89,16 @@ export function TenantProfilePage() {
|
||||
}
|
||||
}, [tenantQuery.data]);
|
||||
|
||||
const hasPersistedOrgConfig =
|
||||
tenantQuery.data?.slug?.toLowerCase() !== "hanmac-family" &&
|
||||
(typeof tenantQuery.data?.config?.orgUnitType === "string" ||
|
||||
typeof tenantQuery.data?.config?.visibility === "string" ||
|
||||
typeof tenantQuery.data?.config?.worksmobileExcluded !== "undefined");
|
||||
const parentQuery = useQuery({
|
||||
queryKey: ["tenants", "list-all"],
|
||||
queryFn: () => fetchAllTenants(),
|
||||
enabled: !!tenantQuery.data && !hasPersistedOrgConfig,
|
||||
});
|
||||
const allTenants = parentQuery.data?.items ?? [];
|
||||
const orgConfigCandidate = tenantQuery.data
|
||||
? {
|
||||
@@ -103,7 +108,8 @@ export function TenantProfilePage() {
|
||||
}
|
||||
: undefined;
|
||||
const canEditOrgConfig = orgConfigCandidate
|
||||
? shouldAllowHanmacOrgConfig(orgConfigCandidate, [
|
||||
? hasPersistedOrgConfig ||
|
||||
shouldAllowHanmacOrgConfig(orgConfigCandidate, [
|
||||
...allTenants,
|
||||
orgConfigCandidate,
|
||||
])
|
||||
@@ -361,7 +367,10 @@ export function TenantProfilePage() {
|
||||
data-testid="tenant-org-unit-type-slot"
|
||||
className="space-y-1"
|
||||
>
|
||||
<Label className="text-sm font-semibold">
|
||||
<Label
|
||||
htmlFor="tenant-org-unit-type"
|
||||
className="text-sm font-semibold"
|
||||
>
|
||||
{t(
|
||||
"ui.admin.tenants.profile.org_unit_type",
|
||||
"조직 세부타입",
|
||||
@@ -385,7 +394,10 @@ export function TenantProfilePage() {
|
||||
</select>
|
||||
</div>
|
||||
<div data-testid="tenant-visibility-slot" className="space-y-1">
|
||||
<Label className="text-sm font-semibold">
|
||||
<Label
|
||||
htmlFor="tenant-visibility"
|
||||
className="text-sm font-semibold"
|
||||
>
|
||||
{t("ui.admin.tenants.profile.visibility", "공개 범위")}
|
||||
</Label>
|
||||
<select
|
||||
@@ -411,7 +423,10 @@ export function TenantProfilePage() {
|
||||
data-testid="tenant-worksmobile-excluded-slot"
|
||||
className="space-y-1"
|
||||
>
|
||||
<Label className="text-sm font-semibold">
|
||||
<Label
|
||||
htmlFor="worksmobileExcluded"
|
||||
className="text-sm font-semibold"
|
||||
>
|
||||
{t(
|
||||
"ui.admin.tenants.profile.worksmobile_sync",
|
||||
"WORKS 연동",
|
||||
|
||||
@@ -624,6 +624,52 @@ describe("TenantWorksmobilePage comparison helpers", () => {
|
||||
).toEqual(["조직: Baron 소속 정보를 WORKS에 반영해야 합니다."]);
|
||||
});
|
||||
|
||||
it("formats grade update reasons with before and after values", () => {
|
||||
expect(
|
||||
formatWorksmobileUpdateDetails({
|
||||
resourceType: "USER",
|
||||
status: "needs_update",
|
||||
baronId: "user-1",
|
||||
externalKey: "user-1",
|
||||
baronName: "신현우",
|
||||
worksmobileName: "신현우",
|
||||
baronGrade: "책임",
|
||||
worksmobileLevelName: "선임",
|
||||
updateReasons: ["grade"],
|
||||
}),
|
||||
).toEqual(["직급: 선임 -> 책임"]);
|
||||
});
|
||||
|
||||
it("formats grade update reasons with matched WORKS membership", () => {
|
||||
expect(
|
||||
formatWorksmobileUpdateDetails({
|
||||
resourceType: "USER",
|
||||
status: "needs_update",
|
||||
baronId: "user-1",
|
||||
externalKey: "user-1",
|
||||
baronName: "연구원",
|
||||
worksmobileName: "연구원",
|
||||
baronGrade: "책임연구원",
|
||||
worksmobileLevelName: "",
|
||||
updateReasons: ["grade"],
|
||||
userMemberships: [
|
||||
{
|
||||
baronOrgId: "1d74bebb-c5a1-49d4-bec4-90f0c89ad21f",
|
||||
baronOrgSlug: "hmeg",
|
||||
baronOrgName: "HmEG",
|
||||
baronGrade: "책임연구원",
|
||||
worksmobileOrgId: "works-hmeg",
|
||||
worksmobileOrgName: "WORKS HmEG",
|
||||
worksmobileDomainName: "baroncs.co.kr",
|
||||
gradeNeedsUpdate: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual([
|
||||
"직급: 없음 -> 책임연구원 (Baron HmEG / WORKS WORKS HmEG)",
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not format phone update details for spaced Korean country code formatting only", () => {
|
||||
expect(
|
||||
formatWorksmobileUpdateDetails({
|
||||
|
||||
@@ -68,6 +68,7 @@ import {
|
||||
formatWorksmobilePersonName,
|
||||
formatWorksmobileSelectionFailureDescription,
|
||||
formatWorksmobileUpdateDetails,
|
||||
formatWorksmobileUserMembershipDetails,
|
||||
getDefaultGroupComparisonFilters,
|
||||
getDefaultUserComparisonFilters,
|
||||
getDefaultWorksmobileComparisonColumns,
|
||||
@@ -813,7 +814,7 @@ const worksmobileComparisonColumnOptions: Array<{
|
||||
{ key: "externalKey", label: "external_key" },
|
||||
{ key: "worksmobileDomain", label: "WORKS 도메인" },
|
||||
{ key: "worksmobile", label: "WORKS" },
|
||||
{ key: "worksmobileOrg", label: "상위 Works 조직" },
|
||||
{ key: "worksmobileOrg", label: "WORKS 조직 매칭" },
|
||||
{ key: "manage", label: "관리" },
|
||||
];
|
||||
|
||||
@@ -832,7 +833,7 @@ const worksmobileComparisonColumnWidths: Record<
|
||||
worksmobileDomain: 160,
|
||||
worksmobileId: 176,
|
||||
worksmobile: 220,
|
||||
worksmobileOrg: 260,
|
||||
worksmobileOrg: 320,
|
||||
manage: 112,
|
||||
};
|
||||
const worksmobileComparisonTableHeadClassName =
|
||||
@@ -1539,7 +1540,7 @@ function ComparisonTable({
|
||||
<div
|
||||
className={worksmobileComparisonTableHeadContentClassName}
|
||||
>
|
||||
상위 Works 조직
|
||||
WORKS 조직 매칭
|
||||
</div>
|
||||
</TableHead>
|
||||
)}
|
||||
@@ -1724,33 +1725,17 @@ function ComparisonTable({
|
||||
)}
|
||||
{isColumnVisible("worksmobileOrg") && (
|
||||
<TableCell>
|
||||
<ComparisonOrgCell
|
||||
name={
|
||||
row.resourceType === "GROUP"
|
||||
? getWorksmobileParentName(row)
|
||||
: row.worksmobilePrimaryOrgName
|
||||
}
|
||||
email={
|
||||
row.resourceType === "GROUP"
|
||||
? getWorksmobileParentEmail(row)
|
||||
: undefined
|
||||
}
|
||||
id={
|
||||
row.resourceType === "GROUP"
|
||||
? row.worksmobileParentId
|
||||
: row.worksmobilePrimaryOrgId
|
||||
}
|
||||
details={
|
||||
row.resourceType === "GROUP"
|
||||
? formatWorksmobileParentOrgDetails(row)
|
||||
: formatWorksmobileOrgDetails(row)
|
||||
}
|
||||
missingLabel={
|
||||
row.resourceType === "GROUP"
|
||||
? "상위 Works 조직 정보 없음"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{row.resourceType === "USER" ? (
|
||||
<ComparisonUserMembershipCell row={row} />
|
||||
) : (
|
||||
<ComparisonOrgCell
|
||||
name={getWorksmobileParentName(row)}
|
||||
email={getWorksmobileParentEmail(row)}
|
||||
id={row.worksmobileParentId}
|
||||
details={formatWorksmobileParentOrgDetails(row)}
|
||||
missingLabel="상위 Works 조직 정보 없음"
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
)}
|
||||
{showManageColumn && isColumnVisible("manage") && (
|
||||
@@ -1893,6 +1878,33 @@ function formatWorksmobileParentOrgDetails(row: WorksmobileComparisonItem) {
|
||||
return details;
|
||||
}
|
||||
|
||||
function ComparisonUserMembershipCell({
|
||||
row,
|
||||
}: {
|
||||
row: WorksmobileComparisonItem;
|
||||
}) {
|
||||
const membershipDetails = formatWorksmobileUserMembershipDetails(row);
|
||||
if (membershipDetails.length > 0) {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{membershipDetails.map((detail) => (
|
||||
<div key={detail} className="text-xs leading-relaxed">
|
||||
{detail}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ComparisonOrgCell
|
||||
name={row.worksmobilePrimaryOrgName}
|
||||
id={row.worksmobilePrimaryOrgId}
|
||||
details={formatWorksmobileOrgDetails(row)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ComparisonOrgCell({
|
||||
name,
|
||||
email,
|
||||
|
||||
@@ -365,6 +365,32 @@ export function formatWorksmobileOrgDetails(row: WorksmobileComparisonItem) {
|
||||
return details;
|
||||
}
|
||||
|
||||
export function formatWorksmobileUserMembershipDetails(
|
||||
row: WorksmobileComparisonItem,
|
||||
) {
|
||||
return (row.userMemberships ?? []).map((membership) => {
|
||||
const baronOrg =
|
||||
membership.baronOrgName?.trim() ||
|
||||
membership.baronOrgSlug?.trim() ||
|
||||
membership.baronOrgId?.trim() ||
|
||||
"Baron 조직";
|
||||
const worksOrg =
|
||||
membership.worksmobileOrgName?.trim() ||
|
||||
membership.worksmobileOrgId?.trim() ||
|
||||
"WORKS 조직 없음";
|
||||
const details = [
|
||||
membership.baronPrimary ? "기본" : "겸직",
|
||||
`Baron ${baronOrg}`,
|
||||
`WORKS ${worksOrg}`,
|
||||
membership.worksmobileLevelName?.trim() ||
|
||||
membership.worksmobileLevelId?.trim()
|
||||
? `직급 ${membership.worksmobileLevelName?.trim() || membership.worksmobileLevelId?.trim()}`
|
||||
: "",
|
||||
].filter(Boolean);
|
||||
return details.join(" / ");
|
||||
});
|
||||
}
|
||||
|
||||
export function formatWorksmobileUpdateDetails(row: WorksmobileComparisonItem) {
|
||||
if (row.status === "missing_in_worksmobile" && row.worksmobileLastError) {
|
||||
return [`최근 실패: ${row.worksmobileLastError}`];
|
||||
@@ -420,6 +446,20 @@ export function formatWorksmobileUpdateDetails(row: WorksmobileComparisonItem) {
|
||||
`사번: ${actualEmployeeNumber} -> ${expectedEmployeeNumber}`,
|
||||
);
|
||||
}
|
||||
const expectedGrade = row.baronGrade?.trim() ?? "";
|
||||
const actualGrade =
|
||||
row.worksmobileLevelName?.trim() ?? row.worksmobileLevelId?.trim() ?? "";
|
||||
if (
|
||||
row.updateReasons?.includes("grade") &&
|
||||
(expectedGrade || actualGrade) &&
|
||||
expectedGrade !== actualGrade
|
||||
) {
|
||||
const membershipContext = formatWorksmobileGradeMembershipContext(row);
|
||||
addDetail(
|
||||
"grade",
|
||||
`직급: ${actualGrade || "없음"} -> ${expectedGrade || "없음"}${membershipContext}`,
|
||||
);
|
||||
}
|
||||
appendWorksmobileUpdateReasonFallbacks(details, row, renderedReasons);
|
||||
return details;
|
||||
}
|
||||
@@ -484,6 +524,17 @@ function formatWorksmobileUpdateReasonFallback(
|
||||
return "전화번호: Baron 전화번호를 WORKS에 반영해야 합니다.";
|
||||
case "employee_number":
|
||||
return "사번: Baron 사번을 WORKS에 반영해야 합니다.";
|
||||
case "grade": {
|
||||
const expectedGrade = row.baronGrade?.trim() ?? "";
|
||||
const actualGrade =
|
||||
row.worksmobileLevelName?.trim() ??
|
||||
row.worksmobileLevelId?.trim() ??
|
||||
"";
|
||||
if (expectedGrade || actualGrade) {
|
||||
return `직급: ${actualGrade || "없음"} -> ${expectedGrade || "없음"}${formatWorksmobileGradeMembershipContext(row)}`;
|
||||
}
|
||||
return "직급: Baron 직급 정보를 WORKS에 반영해야 합니다.";
|
||||
}
|
||||
case "organization":
|
||||
return row.resourceType === "GROUP"
|
||||
? "조직: Baron 조직 정보를 WORKS에 반영해야 합니다."
|
||||
@@ -495,6 +546,32 @@ function formatWorksmobileUpdateReasonFallback(
|
||||
}
|
||||
}
|
||||
|
||||
function formatWorksmobileGradeMembershipContext(
|
||||
row: WorksmobileComparisonItem,
|
||||
) {
|
||||
const membership =
|
||||
row.userMemberships?.find((item) => item.gradeNeedsUpdate) ??
|
||||
row.userMemberships?.find(
|
||||
(item) =>
|
||||
item.baronGrade?.trim() &&
|
||||
item.baronGrade?.trim() === row.baronGrade?.trim(),
|
||||
);
|
||||
if (!membership) {
|
||||
return "";
|
||||
}
|
||||
const baronOrg =
|
||||
membership.baronOrgName?.trim() ||
|
||||
membership.baronOrgSlug?.trim() ||
|
||||
membership.baronOrgId?.trim();
|
||||
const worksOrg =
|
||||
membership.worksmobileOrgName?.trim() ||
|
||||
membership.worksmobileOrgId?.trim();
|
||||
if (!baronOrg && !worksOrg) {
|
||||
return "";
|
||||
}
|
||||
return ` (Baron ${baronOrg || "없음"} / WORKS ${worksOrg || "없음"})`;
|
||||
}
|
||||
|
||||
function normalizeWorksmobilePhoneForCompare(value: string) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
|
||||
Reference in New Issue
Block a user