diff --git a/adminfront/src/features/users/UserCreatePage.tsx b/adminfront/src/features/users/UserCreatePage.tsx index c8849a8f..6e3b6a1a 100644 --- a/adminfront/src/features/users/UserCreatePage.tsx +++ b/adminfront/src/features/users/UserCreatePage.tsx @@ -58,7 +58,11 @@ import { import type { UserSchemaField } from "./userSchemaFields"; import { resolvePersonalTenant } from "./utils/personalTenant"; -type UserFormValues = UserCreateRequest & { metadata: Record }; +type UserFormValues = UserCreateRequest & { + metadata: Record & { + sub_email?: string; + }; +}; type UserCategory = "hanmac" | "external" | "personal"; type PickerTarget = { kind: "appointment"; index: number }; @@ -161,7 +165,9 @@ function UserCreatePage() { position: "", jobTitle: "", role: "user", - metadata: {}, + metadata: { + sub_email: "", + }, }, }); @@ -367,10 +373,22 @@ function UserCreatePage() { const { hanmacFamily: _hanmacFamily, userType: _userType, + sub_email: rawSubEmail, ...formMetadata } = data.metadata ?? {}; + + // Parse sub_email + let sub_email: string[] = []; + if (typeof rawSubEmail === "string" && rawSubEmail.trim() !== "") { + sub_email = rawSubEmail + .split(/[;,\n\r\t]/) + .map((e) => e.trim()) + .filter((e) => e.includes("@")); + } + const metadata: Record = { ...formMetadata, + ...(sub_email.length > 0 ? { sub_email } : { sub_email: [] }), }; const payload: UserCreateRequest = { @@ -580,6 +598,26 @@ function UserCreatePage() { )} +
+ + +
+
- {user.metadata?.secondary_emails && - Array.isArray(user.metadata.secondary_emails) && - user.metadata.secondary_emails.length > 0 && ( + {user.metadata?.sub_email && + Array.isArray(user.metadata.sub_email) && + user.metadata.sub_email.length > 0 && (
- +{user.metadata.secondary_emails.length} + +{user.metadata.sub_email.length}
)} @@ -1058,48 +1084,32 @@ function UserDetailPage() {
- {user.metadata?.secondary_emails && - Array.isArray(user.metadata.secondary_emails) && - user.metadata.secondary_emails.length > 0 && ( -
-
- -
- {(user.metadata.secondary_emails as string[]).map( - (email, idx) => ( -
- - - - {email} -
- ), - )} -
-

- {t( - "msg.admin.users.detail.secondary_emails_help", - "* 보조 이메일은 일괄 등록 또는 외부 연동을 통해 관리됩니다.", - )} -

-
-
- )} +
+
+ + +

+ {t( + "msg.admin.users.detail.sub_email_help", + "* 보조 이메일로도 로그인이 가능하며 계정 찾기 등에 활용될 수 있습니다.", + )} +

+
+
diff --git a/adminfront/src/features/users/components/UserBulkUploadModal.tsx b/adminfront/src/features/users/components/UserBulkUploadModal.tsx index a68a6f81..752dc550 100644 --- a/adminfront/src/features/users/components/UserBulkUploadModal.tsx +++ b/adminfront/src/features/users/components/UserBulkUploadModal.tsx @@ -127,7 +127,7 @@ function hanmacEmailStatusClass(preview?: HanmacImportEmailPreview) { export const downloadUserTemplate = () => { const headers = - "email,secondary_emails,name,phone,role,tenant_slug,department,grade,position,jobTitle,employee_id,tenant_slug1,department1,grade1,position1,jobTitle1,employee_id1"; + "email,sub_email,name,phone,role,tenant_slug,department,grade,position,jobTitle,employee_id,tenant_slug1,department1,grade1,position1,jobTitle1,employee_id1"; const example = "user1@example.com,sub1@test.com;sub2@test.com,홍길동,010-1234-5678,user,tenant-slug,개발팀,수석,팀장,프론트엔드,EMP001,second-tenant,센터,책임,,Architecture,EMP002"; const blob = new Blob([`${headers}\n${example}`], { @@ -297,7 +297,7 @@ export function UserBulkUploadModal({ const downloadTemplate = () => { const headers = - "email,secondary_emails,name,phone,role,tenant_slug,department,grade,position,jobTitle,employee_id,tenant_slug1,department1,grade1,position1,jobTitle1,employee_id1"; + "email,sub_email,name,phone,role,tenant_slug,department,grade,position,jobTitle,employee_id,tenant_slug1,department1,grade1,position1,jobTitle1,employee_id1"; const example = "user1@example.com,sub1@test.com;sub2@test.com,홍길동,010-1234-5678,user,tenant-slug,개발팀,수석,팀장,프론트엔드,EMP001,second-tenant,센터,책임,,Architecture,EMP002"; const blob = new Blob([`${headers}\n${example}`], { diff --git a/adminfront/src/features/users/utils/csvParser.ts b/adminfront/src/features/users/utils/csvParser.ts index ca8e65e5..87d83a17 100644 --- a/adminfront/src/features/users/utils/csvParser.ts +++ b/adminfront/src/features/users/utils/csvParser.ts @@ -350,10 +350,11 @@ function applySecondaryEmailMetadata( value: string, ) { const emails = splitEmailTokens(value); - item.metadata.secondary_emails = uniqueEmails([ - ...metadataEmailList(item.metadata.secondary_emails), + item.metadata.sub_email = uniqueEmails([ + ...metadataEmailList(item.metadata.sub_email), ...emails, ]); + addWorksmobileAliasEmails(item, emails); } function splitOrganizationPath(value: string) { diff --git a/adminfront/tests/users_bulk.spec.ts b/adminfront/tests/users_bulk.spec.ts index dd0bdd25..f87d02be 100644 --- a/adminfront/tests/users_bulk.spec.ts +++ b/adminfront/tests/users_bulk.spec.ts @@ -302,10 +302,7 @@ test.describe("Users Bulk Upload", () => { const payload = JSON.parse(bulkPayload); expect(payload.users[0].tenantSlug).toBe("primary-tenant"); expect(payload.users[0].metadata.employee_id).toBe("EMP001"); - expect(payload.users[0].metadata.sub_email).toBe( - "dual.alias@hanmaceng.co.kr", - ); - expect(payload.users[0].metadata.secondary_emails).toEqual([ + expect(payload.users[0].metadata.sub_email).toEqual([ "dual.alias@hanmaceng.co.kr", ]); expect(payload.users[0].metadata.aliasEmails).toEqual([ diff --git a/adminfront/tests/users_bulk_secondary.spec.ts b/adminfront/tests/users_bulk_secondary.spec.ts index 4488bc30..d7a03e7b 100644 --- a/adminfront/tests/users_bulk_secondary.spec.ts +++ b/adminfront/tests/users_bulk_secondary.spec.ts @@ -66,7 +66,7 @@ test.describe("Users Bulk Upload Secondary Emails", () => { await page.getByRole("menuitem", { name: /일괄 임포트|일괄 등록|Bulk Import/i }).click(); // Create a mock CSV with secondary_emails - const csvContent = `email,secondary_emails,name,phone,role,tenant_slug\ntest@example.com,sub1@test.com;sub2@test.com,홍길동,010-1234-5678,user,tenant-slug`; + const csvContent = `email,sub_email,name,phone,role,tenant_slug\ntest@example.com,sub1@test.com;sub2@test.com,홍길동,010-1234-5678,user,tenant-slug`; const fileChooserPromise = page.waitForEvent('filechooser'); await page.getByText(/파일 선택|Change file|Select file/i).click(); @@ -89,7 +89,7 @@ test.describe("Users Bulk Upload Secondary Emails", () => { expect(bulkPayload.users).toHaveLength(1); // The most important check - does it parse to the metadata - expect(bulkPayload.users[0].metadata.secondary_emails).toContain("sub1@test.com"); - expect(bulkPayload.users[0].metadata.secondary_emails).toContain("sub2@test.com"); + expect(bulkPayload.users[0].metadata.sub_email).toContain("sub1@test.com"); + expect(bulkPayload.users[0].metadata.sub_email).toContain("sub2@test.com"); }); }); \ No newline at end of file diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index 05ecfd9b..85b98c1d 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -1769,7 +1769,7 @@ func collectEmailList(traits map[string]any, primaryEmail string) []string { } } - if raw, ok := traits["secondary_emails"]; ok { + if raw, ok := traits["sub_email"]; ok { switch value := raw.(type) { case []string: for _, email := range value { diff --git a/backend/internal/handler/user_handler.go b/backend/internal/handler/user_handler.go index 3e0b2644..03faf338 100644 --- a/backend/internal/handler/user_handler.go +++ b/backend/internal/handler/user_handler.go @@ -1226,7 +1226,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error { valid := true // Collect all emails allEmails := []string{userEmail} - if secondaryRaw, exists := item.Metadata["secondary_emails"]; exists { + if secondaryRaw, exists := item.Metadata["sub_email"]; exists { if secondaryEmails, ok := secondaryRaw.([]interface{}); ok { for _, se := range secondaryEmails { if seStr, ok := se.(string); ok { @@ -2052,7 +2052,7 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error { userPhone := extractTraitString(traits, "phone_number") allEmails := []string{userEmail} - if secondaryRaw, exists := traits["secondary_emails"]; exists { + if secondaryRaw, exists := traits["sub_email"]; exists { if secondaryEmails, ok := secondaryRaw.([]interface{}); ok { for _, se := range secondaryEmails { if seStr, ok := se.(string); ok {