diff --git a/adminfront/src/features/tenants/routes/TenantAdminsAndOwnersTab.tsx b/adminfront/src/features/tenants/routes/TenantAdminsAndOwnersTab.tsx index da160f0f..dedf8d65 100644 --- a/adminfront/src/features/tenants/routes/TenantAdminsAndOwnersTab.tsx +++ b/adminfront/src/features/tenants/routes/TenantAdminsAndOwnersTab.tsx @@ -83,16 +83,27 @@ export function TenantAdminsAndOwnersTab() { const addOwnerMutation = useMutation({ mutationFn: (userId: string) => addTenantOwner(tenantId, userId), onMutate: async (userId) => { - await queryClient.cancelQueries({ queryKey: ["tenant-owners", tenantId] }); - const previousOwners = queryClient.getQueryData(["tenant-owners", tenantId]); - + await queryClient.cancelQueries({ + queryKey: ["tenant-owners", tenantId], + }); + const previousOwners = queryClient.getQueryData([ + "tenant-owners", + tenantId, + ]); + // Optimistically add to the list to prevent immediate double clicks - const addedUser = searchResults.find(u => u.id === userId); + const addedUser = searchResults.find((u) => u.id === userId); if (addedUser) { - queryClient.setQueryData(["tenant-owners", tenantId], old => { - if (!old) return [{ id: userId, name: addedUser.name, email: addedUser.email }]; - if (old.some(o => o.id === userId)) return old; - return [...old, { id: userId, name: addedUser.name, email: addedUser.email }]; + queryClient.setQueryData(["tenant-owners", tenantId], (old) => { + if (!old) + return [ + { id: userId, name: addedUser.name, email: addedUser.email }, + ]; + if (old.some((o) => o.id === userId)) return old; + return [ + ...old, + { id: userId, name: addedUser.name, email: addedUser.email }, + ]; }); } return { previousOwners }; @@ -100,7 +111,9 @@ export function TenantAdminsAndOwnersTab() { onSuccess: () => { // Delay invalidation slightly to give the backend outbox time to process setTimeout(() => { - queryClient.invalidateQueries({ queryKey: ["tenant-owners", tenantId] }); + queryClient.invalidateQueries({ + queryKey: ["tenant-owners", tenantId], + }); }, 1000); toast.success( t("msg.admin.tenants.owners.add_success", "소유자가 추가되었습니다."), @@ -109,7 +122,10 @@ export function TenantAdminsAndOwnersTab() { }, onError: (err: AxiosError<{ error?: string }>, userId, context) => { if (context?.previousOwners) { - queryClient.setQueryData(["tenant-owners", tenantId], context.previousOwners); + queryClient.setQueryData( + ["tenant-owners", tenantId], + context.previousOwners, + ); } toast.error( err.response?.data?.error || @@ -121,16 +137,23 @@ export function TenantAdminsAndOwnersTab() { const removeOwnerMutation = useMutation({ mutationFn: (userId: string) => removeTenantOwner(tenantId, userId), onMutate: async (userId) => { - await queryClient.cancelQueries({ queryKey: ["tenant-owners", tenantId] }); - const previousOwners = queryClient.getQueryData(["tenant-owners", tenantId]); - queryClient.setQueryData(["tenant-owners", tenantId], old => - old ? old.filter(o => o.id !== userId) : [] + await queryClient.cancelQueries({ + queryKey: ["tenant-owners", tenantId], + }); + const previousOwners = queryClient.getQueryData([ + "tenant-owners", + tenantId, + ]); + queryClient.setQueryData(["tenant-owners", tenantId], (old) => + old ? old.filter((o) => o.id !== userId) : [], ); return { previousOwners }; }, onSuccess: () => { setTimeout(() => { - queryClient.invalidateQueries({ queryKey: ["tenant-owners", tenantId] }); + queryClient.invalidateQueries({ + queryKey: ["tenant-owners", tenantId], + }); }, 1000); toast.success( t( @@ -141,7 +164,10 @@ export function TenantAdminsAndOwnersTab() { }, onError: (err: AxiosError<{ error?: string }>, userId, context) => { if (context?.previousOwners) { - queryClient.setQueryData(["tenant-owners", tenantId], context.previousOwners); + queryClient.setQueryData( + ["tenant-owners", tenantId], + context.previousOwners, + ); } toast.error( err.response?.data?.error || @@ -153,22 +179,35 @@ export function TenantAdminsAndOwnersTab() { const addAdminMutation = useMutation({ mutationFn: (userId: string) => addTenantAdmin(tenantId, userId), onMutate: async (userId) => { - await queryClient.cancelQueries({ queryKey: ["tenant-admins", tenantId] }); - const previousAdmins = queryClient.getQueryData(["tenant-admins", tenantId]); - - const addedUser = searchResults.find(u => u.id === userId); + await queryClient.cancelQueries({ + queryKey: ["tenant-admins", tenantId], + }); + const previousAdmins = queryClient.getQueryData([ + "tenant-admins", + tenantId, + ]); + + const addedUser = searchResults.find((u) => u.id === userId); if (addedUser) { - queryClient.setQueryData(["tenant-admins", tenantId], old => { - if (!old) return [{ id: userId, name: addedUser.name, email: addedUser.email }]; - if (old.some(a => a.id === userId)) return old; - return [...old, { id: userId, name: addedUser.name, email: addedUser.email }]; + queryClient.setQueryData(["tenant-admins", tenantId], (old) => { + if (!old) + return [ + { id: userId, name: addedUser.name, email: addedUser.email }, + ]; + if (old.some((a) => a.id === userId)) return old; + return [ + ...old, + { id: userId, name: addedUser.name, email: addedUser.email }, + ]; }); } return { previousAdmins }; }, onSuccess: () => { setTimeout(() => { - queryClient.invalidateQueries({ queryKey: ["tenant-admins", tenantId] }); + queryClient.invalidateQueries({ + queryKey: ["tenant-admins", tenantId], + }); }, 1000); toast.success( t("msg.admin.tenants.admins.add_success", "관리자가 추가되었습니다."), @@ -177,7 +216,10 @@ export function TenantAdminsAndOwnersTab() { }, onError: (err: AxiosError<{ error?: string }>, userId, context) => { if (context?.previousAdmins) { - queryClient.setQueryData(["tenant-admins", tenantId], context.previousAdmins); + queryClient.setQueryData( + ["tenant-admins", tenantId], + context.previousAdmins, + ); } toast.error( err.response?.data?.error || @@ -189,16 +231,23 @@ export function TenantAdminsAndOwnersTab() { const removeAdminMutation = useMutation({ mutationFn: (userId: string) => removeTenantAdmin(tenantId, userId), onMutate: async (userId) => { - await queryClient.cancelQueries({ queryKey: ["tenant-admins", tenantId] }); - const previousAdmins = queryClient.getQueryData(["tenant-admins", tenantId]); - queryClient.setQueryData(["tenant-admins", tenantId], old => - old ? old.filter(a => a.id !== userId) : [] + await queryClient.cancelQueries({ + queryKey: ["tenant-admins", tenantId], + }); + const previousAdmins = queryClient.getQueryData([ + "tenant-admins", + tenantId, + ]); + queryClient.setQueryData(["tenant-admins", tenantId], (old) => + old ? old.filter((a) => a.id !== userId) : [], ); return { previousAdmins }; }, onSuccess: () => { setTimeout(() => { - queryClient.invalidateQueries({ queryKey: ["tenant-admins", tenantId] }); + queryClient.invalidateQueries({ + queryKey: ["tenant-admins", tenantId], + }); }, 1000); toast.success( t("msg.admin.tenants.admins.remove_success", "권한이 회수되었습니다."), @@ -206,7 +255,10 @@ export function TenantAdminsAndOwnersTab() { }, onError: (err: AxiosError<{ error?: string }>, userId, context) => { if (context?.previousAdmins) { - queryClient.setQueryData(["tenant-admins", tenantId], context.previousAdmins); + queryClient.setQueryData( + ["tenant-admins", tenantId], + context.previousAdmins, + ); } toast.error( err.response?.data?.error || diff --git a/adminfront/src/features/users/UserDetailPage.tsx b/adminfront/src/features/users/UserDetailPage.tsx index 9c6f000b..a39d21d0 100644 --- a/adminfront/src/features/users/UserDetailPage.tsx +++ b/adminfront/src/features/users/UserDetailPage.tsx @@ -161,7 +161,9 @@ function UserDetailPage() { const [error, setError] = React.useState(null); const [successMsg, setSuccessMsg] = React.useState(null); const [isPasswordResetOpen, setIsPasswordResetOpen] = React.useState(false); - const [generatedPassword, setGeneratedPassword] = React.useState(null); + const [generatedPassword, setGeneratedPassword] = React.useState< + string | null + >(null); const { data: profile } = useQuery({ queryKey: ["me"], @@ -470,7 +472,8 @@ function UserDetailPage() { )}

- * 사용자의 주된 정체성을 결정하는 대표 조직을 선택합니다. + * 사용자의 주된 정체성을 결정하는 대표 조직을 + 선택합니다.

@@ -571,7 +574,10 @@ function UserDetailPage() { {...register("role")} > @@ -661,7 +670,9 @@ function UserDetailPage() {
{userAffiliatedTenants.map((t) => { - const tDetail = tenants.find((tenant) => tenant.id === t.id); + const tDetail = tenants.find( + (tenant) => tenant.id === t.id, + ); const schema = (tDetail?.config?.userSchema || []) as UserSchemaField[]; return ( @@ -707,7 +718,8 @@ function UserDetailPage() { )}

- 사용자의 비밀번호를 강제로 재설정하고 새 비밀번호를 생성합니다. + 사용자의 비밀번호를 강제로 재설정하고 새 비밀번호를 + 생성합니다.

- @@ -746,7 +772,11 @@ function UserDetailPage() { {generatedPassword}

- @@ -770,7 +800,11 @@ function UserDetailPage() {
- + {user.status === "active" ? "Active" : "Inactive"} {user.role} diff --git a/adminfront/tests/users_schema.spec.ts b/adminfront/tests/users_schema.spec.ts index da92e4ed..23108bcf 100644 --- a/adminfront/tests/users_schema.spec.ts +++ b/adminfront/tests/users_schema.spec.ts @@ -149,9 +149,6 @@ test.describe("User Schema Dynamic Form", () => { }) => { await page.goto("/users/u-1"); - - - // 섹션 헤더 확인 const header = page .getByText(/테넌트별 프로필 관리|Per-tenant Profile/i) @@ -176,9 +173,6 @@ test.describe("User Schema Dynamic Form", () => { }) => { await page.goto("/users/u-1"); - - - const empIdInput = page.locator('input[id*="emp_id"]'); await empIdInput.waitFor({ state: "visible" }); await empIdInput.fill("invalid"); diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index 6a79a336..85792667 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -216,7 +216,7 @@ func (h *AuthHandler) CheckLoginID(c *fiber.Ctx) error { if err != nil { return errorJSON(c, fiber.StatusServiceUnavailable, "Identity provider unavailable") } - + if exists { return c.JSON(fiber.Map{"available": false, "message": "ID already registered"}) } @@ -4058,7 +4058,8 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe // Fallback to Hydra introspection. This is expected for API calls using Bearer tokens. slog.Debug("Kratos cookie session absent, falling back to Hydra token", "error", err.Error()) profile, err = h.getHydraProfile(c.Context(), token) - } } else if cookie != "" { + } + } else if cookie != "" { profile, err = h.getKratosProfileWithCookie(cookie) } } diff --git a/backend/internal/handler/user_handler.go b/backend/internal/handler/user_handler.go index d0c21069..3c469c7b 100644 --- a/backend/internal/handler/user_handler.go +++ b/backend/internal/handler/user_handler.go @@ -579,7 +579,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error { finalLoginID := extractTraitString(attributes, "id") userEmail := email userPhone := normalizePhoneNumber(item.Phone) - + if err := domain.ValidateLoginID(finalLoginID, userEmail, userPhone); err != nil { results = append(results, bulkUserResult{Email: email, Success: false, Message: err.Error()}) continue diff --git a/backend/internal/handler/user_handler_test.go b/backend/internal/handler/user_handler_test.go index 9e69063e..58d88eb6 100644 --- a/backend/internal/handler/user_handler_test.go +++ b/backend/internal/handler/user_handler_test.go @@ -128,16 +128,16 @@ func TestUserHandler_BulkCreateUsers(t *testing.T) { payload := map[string]interface{}{ "users": []map[string]interface{}{ { - "email": "user1@test.com", - "name": "User One", - "tenantSlug": "test-tenant", - "metadata": map[string]interface{}{"emp_id": "E001"}, + "email": "user1@test.com", + "name": "User One", + "tenantSlug": "test-tenant", + "metadata": map[string]interface{}{"emp_id": "E001"}, }, { - "email": "user2@test.com", - "name": "User Two", - "tenantSlug": "test-tenant", - "metadata": map[string]interface{}{"emp_id": "E002"}, + "email": "user2@test.com", + "name": "User Two", + "tenantSlug": "test-tenant", + "metadata": map[string]interface{}{"emp_id": "E002"}, }, }, }