diff --git a/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx b/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx index f6aa4060..8dd1f01e 100644 --- a/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx +++ b/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx @@ -229,7 +229,8 @@ const MemberTable: React.FC<{ }); const removeMutation = useMutation({ - mutationFn: (userId: string) => updateUser(userId, { tenantSlug: "" }), + mutationFn: (userId: string) => + updateUser(userId, { tenantSlug, isRemoveTenant: true }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["tenants-full-tree-v2"] }); toast.success(t("msg.info.saved_success", "조직에서 제외되었습니다.")); diff --git a/backend/internal/handler/user_handler.go b/backend/internal/handler/user_handler.go index 8502e096..587e4cf0 100644 --- a/backend/internal/handler/user_handler.go +++ b/backend/internal/handler/user_handler.go @@ -1588,27 +1588,54 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error { } } } else { - // Normal update: replace primary company code and ensure it's in existingCodes - traits["companyCode"] = code - // Resolve TenantID for Kratos Trait - if h.TenantService != nil && code != "" { - if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), code); err == nil && tenant != nil { - traits["tenant_id"] = tenant.ID - } - } + // Normal update (Move): replace primary company code and remove the old one from existingCodes + currentPrimary := extractTraitString(traits, "companyCode") + if currentPrimary != "" && currentPrimary != code { + // Remove old primary from existingCodes + var newCodes []string + for _, existing := range existingCodes { + if existing != currentPrimary { + newCodes = append(newCodes, existing) + } + } + existingCodes = newCodes - found := false - for _, existing := range existingCodes { - if existing == code { - found = true - break - } - } - if !found && code != "" { - existingCodes = append(existingCodes, code) - } - } - } + // [Keto Sync] Remove membership for the old tenant + if h.TenantService != nil && h.KetoOutboxRepo != nil { + go func(removedSlug string) { + bgCtx := context.Background() + if t, err := h.TenantService.GetTenantBySlug(bgCtx, removedSlug); err == nil && t != nil { + _ = h.KetoOutboxRepo.Create(bgCtx, &domain.KetoOutbox{ + Namespace: "Tenant", + Object: t.ID, + Relation: "members", + Subject: "User:" + userID, + Action: domain.KetoOutboxActionDelete, + }) + } + }(currentPrimary) + } + } + + traits["companyCode"] = code + // Resolve TenantID for Kratos Trait + if h.TenantService != nil && code != "" { + if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), code); err == nil && tenant != nil { + traits["tenant_id"] = tenant.ID + } + } + + found := false + for _, existing := range existingCodes { + if existing == code { + found = true + break + } + } + if !found && code != "" { + existingCodes = append(existingCodes, code) + } + } } // Deduplicate and save back companyCodes var codesToSave []string