From 62b1938c42a0af3b9e332b69ee6530223577f5af Mon Sep 17 00:00:00 2001 From: chan Date: Fri, 29 May 2026 11:07:59 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EB=B3=B4=EC=A1=B0=20=EC=9D=B4?= =?UTF-8?q?=EB=A9=94=EC=9D=BC=20=ED=82=A4=EA=B0=92=EC=9D=84=20sub=5Femail?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=EC=9D=BC=20=EB=B0=8F=20=EC=88=98=EB=8F=99?= =?UTF-8?q?=20=ED=8F=BC=20=EC=B6=94=EA=B0=80=20(#917)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `secondary_emails` 대신 `sub_email`을 키값으로 사용하도록 전면 수정 - 관리자 화면의 수동 사용자 생성(Create) 및 수정(Detail) 폼에 `sub_email` 입력 필드 추가 - CSV 템플릿의 컬럼명을 `sub_email`로 변경 - 백엔드의 Kratos Traits 조회 및 배열 추출 로직을 `sub_email` 기준으로 업데이트 - E2E 테스트(`users_bulk.spec.ts`, `users_bulk_secondary.spec.ts`)에서 `sub_email` 검증하도록 수정 및 통과 확인 --- .../src/features/users/UserCreatePage.tsx | 42 ++++++- .../src/features/users/UserDetailPage.tsx | 110 ++++++++++-------- .../users/components/UserBulkUploadModal.tsx | 4 +- .../src/features/users/utils/csvParser.ts | 5 +- adminfront/tests/users_bulk.spec.ts | 5 +- adminfront/tests/users_bulk_secondary.spec.ts | 6 +- backend/internal/handler/auth_handler.go | 2 +- backend/internal/handler/user_handler.go | 4 +- 8 files changed, 112 insertions(+), 66 deletions(-) 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 {