From 7401454bc0bd9333850a25fc26695c41d6d9870b Mon Sep 17 00:00:00 2001 From: Lectom Date: Thu, 28 May 2026 08:53:28 +0900 Subject: [PATCH] fix(adminfront): guard employee ID metadata in GPO priority swap --- .../users/components/UserBulkUploadModal.tsx | 78 +------------ .../generalPlanningOfficePriority.test.ts | 104 ++++++++++++++++++ .../utils/generalPlanningOfficePriority.ts | 85 ++++++++++++++ 3 files changed, 191 insertions(+), 76 deletions(-) create mode 100644 adminfront/src/features/users/utils/generalPlanningOfficePriority.test.ts create mode 100644 adminfront/src/features/users/utils/generalPlanningOfficePriority.ts diff --git a/adminfront/src/features/users/components/UserBulkUploadModal.tsx b/adminfront/src/features/users/components/UserBulkUploadModal.tsx index dd95fe62..a4bf3adb 100644 --- a/adminfront/src/features/users/components/UserBulkUploadModal.tsx +++ b/adminfront/src/features/users/components/UserBulkUploadModal.tsx @@ -23,7 +23,6 @@ import { ScrollArea } from "../../../components/ui/scroll-area"; import { type BulkUserItem, type BulkUserResult, - type TenantSummary, bulkCreateUsers, createTenant, fetchAllTenants, @@ -41,6 +40,7 @@ import { type HanmacImportEmailPreview, buildHanmacImportEmailPreview, } from "../utils/hanmacImportEmail"; +import { applyGeneralPlanningOfficePriority } from "../utils/generalPlanningOfficePriority"; interface UserBulkUploadModalProps { onSuccess?: () => void; @@ -125,19 +125,6 @@ function hanmacEmailStatusClass(preview?: HanmacImportEmailPreview) { return "text-muted-foreground"; } -function isUnderGeneralPlanningOffice( - tenantSlug: string, - tenants: TenantSummary[], -): boolean { - let current = tenants.find((t) => t.slug === tenantSlug); - while (current) { - if (current.name === "총괄기획실") return true; - if (!current.parentId) break; - current = tenants.find((t) => t.id === current?.parentId); - } - return false; -} - export const downloadUserTemplate = () => { const headers = "email,name,phone,role,tenant_slug,department,grade,position,jobTitle,employee_id,tenant_slug1,department1,grade1,position1,jobTitle1,employee_id1"; @@ -292,68 +279,7 @@ export function UserBulkUploadModal({ } return previewData.map((user, index) => { - let finalUser = { ...user }; - - // [Issue 868] Swap logic for '총괄기획실' (General Planning Office) - // 만약 두 번째 테넌트가 '총괄기획실' 산하이고, 첫 번째 테넌트가 아니면 서로 맞바꿉니다. - if ( - finalUser.additionalAppointments && - finalUser.additionalAppointments.length > 0 - ) { - const firstAdditional = finalUser.additionalAppointments[0]; - const secondarySlug = firstAdditional.tenantSlug; - - if ( - secondarySlug && - isUnderGeneralPlanningOffice(secondarySlug, tenants) - ) { - if (!isUnderGeneralPlanningOffice(finalUser.tenantSlug || "", tenants)) { - // Perform swap - const primary = { - tenantSlug: finalUser.tenantSlug, - tenantImport: finalUser.tenantImport, - department: finalUser.department, - grade: finalUser.grade, - position: finalUser.position, - jobTitle: finalUser.jobTitle, - employee_id: finalUser.metadata?.employee_id, - }; - - finalUser.tenantSlug = firstAdditional.tenantSlug; - if (finalUser.tenantImport) { - finalUser.tenantImport = { - ...finalUser.tenantImport, - slug: firstAdditional.tenantSlug || "", - name: firstAdditional.tenantName || firstAdditional.tenantSlug || "", - }; - } - finalUser.department = firstAdditional.department; - finalUser.grade = firstAdditional.grade; - finalUser.position = firstAdditional.position; - finalUser.jobTitle = firstAdditional.jobTitle; - if (finalUser.metadata) { - finalUser.metadata.employee_id = - firstAdditional.metadata?.employee_id; - } - - finalUser.additionalAppointments = [ - { - ...firstAdditional, - tenantSlug: primary.tenantSlug, - department: primary.department, - grade: primary.grade, - position: primary.position, - jobTitle: primary.jobTitle, - metadata: { - ...firstAdditional.metadata, - employee_id: primary.employee_id, - }, - }, - ...finalUser.additionalAppointments.slice(1), - ]; - } - } - } + const finalUser = applyGeneralPlanningOfficePriority(user, tenants); const key = tenantImportKeyFromUser(finalUser); const resolvedTenant = key ? tenantByKey.get(key) : undefined; diff --git a/adminfront/src/features/users/utils/generalPlanningOfficePriority.test.ts b/adminfront/src/features/users/utils/generalPlanningOfficePriority.test.ts new file mode 100644 index 00000000..e6c35523 --- /dev/null +++ b/adminfront/src/features/users/utils/generalPlanningOfficePriority.test.ts @@ -0,0 +1,104 @@ +import type { BulkUserItem, TenantSummary } from "../../../lib/adminApi"; +import { applyGeneralPlanningOfficePriority } from "./generalPlanningOfficePriority"; + +function tenant( + id: string, + name: string, + slug: string, + parentId?: string, +): TenantSummary { + return { + id, + type: "COMPANY", + name, + slug, + description: "", + status: "active", + parentId, + memberCount: 0, + createdAt: "", + updatedAt: "", + }; +} + +describe("applyGeneralPlanningOfficePriority", () => { + it("promotes the general planning office appointment and preserves string employee IDs", () => { + const user: BulkUserItem = { + email: "dual@test.com", + name: "Dual User", + tenantSlug: "hanmac-tech", + department: "개발팀", + grade: "책임", + position: "팀장", + jobTitle: "Backend", + metadata: { + employee_id: "EMP001", + }, + additionalAppointments: [ + { + tenantSlug: "planning-team", + tenantName: "경영기획팀", + department: "센터", + grade: "수석", + jobTitle: "Architecture", + metadata: { + employee_id: "EMP002", + }, + }, + ], + }; + + const result = applyGeneralPlanningOfficePriority(user, [ + tenant("gpo", "총괄기획실", "gpo"), + tenant("planning", "경영기획팀", "planning-team", "gpo"), + tenant("tech", "한맥기술", "hanmac-tech"), + ]); + + expect(result.tenantSlug).toBe("planning-team"); + expect(result.department).toBe("센터"); + expect(result.grade).toBe("수석"); + expect(result.jobTitle).toBe("Architecture"); + expect(result.metadata.employee_id).toBe("EMP002"); + expect(result.additionalAppointments?.[0]).toMatchObject({ + tenantSlug: "hanmac-tech", + department: "개발팀", + grade: "책임", + position: "팀장", + jobTitle: "Backend", + metadata: { + employee_id: "EMP001", + }, + }); + }); + + it("does not write non-string employee IDs into string metadata", () => { + const user: BulkUserItem = { + email: "dual@test.com", + name: "Dual User", + tenantSlug: "hanmac-tech", + metadata: { + employee_id: "EMP001", + }, + additionalAppointments: [ + { + tenantSlug: "gpo", + tenantName: "총괄기획실", + metadata: { + employee_id: 1002, + }, + }, + ], + }; + + const result = applyGeneralPlanningOfficePriority(user, [ + tenant("gpo", "총괄기획실", "gpo"), + tenant("tech", "한맥기술", "hanmac-tech"), + ]); + + expect(result.tenantSlug).toBe("gpo"); + expect(result.metadata.employee_id).toBeUndefined(); + expect(result.additionalAppointments?.[0].metadata).toMatchObject({ + employee_id: "EMP001", + }); + }); +}); diff --git a/adminfront/src/features/users/utils/generalPlanningOfficePriority.ts b/adminfront/src/features/users/utils/generalPlanningOfficePriority.ts new file mode 100644 index 00000000..9171df42 --- /dev/null +++ b/adminfront/src/features/users/utils/generalPlanningOfficePriority.ts @@ -0,0 +1,85 @@ +import type { BulkUserItem, TenantSummary } from "../../../lib/adminApi"; + +export function applyGeneralPlanningOfficePriority( + user: BulkUserItem, + tenants: TenantSummary[], +): BulkUserItem { + const firstAdditional = user.additionalAppointments?.[0]; + const secondarySlug = firstAdditional?.tenantSlug; + + if ( + !firstAdditional || + !secondarySlug || + !isUnderGeneralPlanningOffice(secondarySlug, tenants) || + isUnderGeneralPlanningOffice(user.tenantSlug || "", tenants) + ) { + return user; + } + + const primaryEmployeeId = stringValue(user.metadata.employee_id); + const secondaryEmployeeId = stringValue( + firstAdditional.metadata?.employee_id, + ); + + const metadata = { ...user.metadata }; + if (secondaryEmployeeId) { + metadata.employee_id = secondaryEmployeeId; + } else { + delete metadata.employee_id; + } + + const primaryAppointmentMetadata: Record = { + ...firstAdditional.metadata, + }; + if (primaryEmployeeId) { + primaryAppointmentMetadata.employee_id = primaryEmployeeId; + } else { + delete primaryAppointmentMetadata.employee_id; + } + + return { + ...user, + tenantSlug: firstAdditional.tenantSlug, + tenantImport: user.tenantImport + ? { + ...user.tenantImport, + slug: firstAdditional.tenantSlug || "", + name: firstAdditional.tenantName || firstAdditional.tenantSlug || "", + } + : user.tenantImport, + department: firstAdditional.department, + grade: firstAdditional.grade, + position: firstAdditional.position, + jobTitle: firstAdditional.jobTitle, + metadata, + additionalAppointments: [ + { + ...firstAdditional, + tenantSlug: user.tenantSlug, + department: user.department, + grade: user.grade, + position: user.position, + jobTitle: user.jobTitle, + metadata: primaryAppointmentMetadata, + }, + ...(user.additionalAppointments?.slice(1) ?? []), + ], + }; +} + +function isUnderGeneralPlanningOffice( + tenantSlug: string, + tenants: TenantSummary[], +): boolean { + let current = tenants.find((tenant) => tenant.slug === tenantSlug); + while (current) { + if (current.name === "총괄기획실") return true; + if (!current.parentId) break; + current = tenants.find((tenant) => tenant.id === current?.parentId); + } + return false; +} + +function stringValue(value: unknown) { + return typeof value === "string" ? value : undefined; +}